Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encapsulated error handling #3261

Merged
merged 11 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 15 additions & 5 deletions docs/Errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,19 @@ To customize this behavior you should use [`setErrorHandler`](Server.md#seterror
From the [Hooks documentation](Hooks.md#manage-errors-from-a-hook):
> If you get an error during the execution of your hook, just pass it to `done()` and Fastify will automatically close the request and send the appropriate error code to the user.

If you have defined a custom error handler using `setErrorHandler` the error will be routed there, otherwise it will be routed to Fastify’s generic error handler. Fastify's generic error handler will use the header and status code in the Error object if it exists. The headers and status code will not be automatically set if a custom error handler is provided.
When a custom error handler has been defined through
[`setErrorHandler`](./Server.md#seterrorhandler), the custom error handler will
receive the error passed to the `done()` callback (or through other supported
automatic error handling mechanisms). If `setErrorHandler` has been used
multiple times to define multiple handlers, the error will be routed to the most
precedent handler defined within the error [encapsulation context](./Encapsulation.md).
Error handlers are fully encapsulated, so a `setErrorHandler` call within a
plugin will limit the error handler to that plugin's context.

The root error handler is Fastify's generic error handler. This error handler
will use the headers and status code in the `Error` object, if they exist. The
headers and status code will not be automatically set if a custom error handler
is provided.

Some things to consider in your custom error handler:

Expand All @@ -46,10 +58,8 @@ Some things to consider in your custom error handler:
- strings, buffers, and streams are sent to the client, with appropriate headers (no serialization)

- You can throw a new error in your custom error handler
- errors (new error or the received error parameter re-thrown) - will trigger the `onError` lifecycle hook and send the error to the user
- an error will not be triggered twice from a lifecycle hook - Fastify internally monitors the error invocation to avoid infinite loops for errors thrown in the reply phases of the lifecycle. (those after the route handler)
- errors sent or thrown by a custom error handler will be routed to Fastify's default error handler

- errors (new error or the received error parameter re-thrown) - will call the parent `errorHandler`.
- `onError` hook will be triggered once only for the first error being thrown.

<a name="fastify-error-codes"></a>
### Fastify Error Codes
Expand Down
25 changes: 5 additions & 20 deletions fastify.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const getSecuredInitialConfig = require('./lib/initialConfigValidation')
const override = require('./lib/pluginOverride')
const warning = require('./lib/warnings')
const { defaultInitOptions } = getSecuredInitialConfig
const setErrorHeaders = require('./lib/setErrorHeaders')

const {
FST_ERR_BAD_URL,
Expand All @@ -54,6 +53,8 @@ const {
appendStackTrace
} = require('./lib/errors')

const { buildErrorHandler } = require('./lib/error-handler.js')

const onBadUrlContext = {
config: {
},
Expand All @@ -74,22 +75,6 @@ function defaultBuildPrettyMeta (route) {
return Object.assign({}, cleanKeys)
}

function defaultErrorHandler (error, request, reply) {
setErrorHeaders(error, reply)
if (reply.statusCode < 500) {
reply.log.info(
{ res: reply, err: error },
error && error.message
)
} else {
reply.log.error(
{ req: request, res: reply, err: error },
error && error.message
)
}
reply.send(error)
}

function fastify (options) {
// Options validations
options = options || {}
Expand Down Expand Up @@ -211,7 +196,7 @@ function fastify (options) {
[kHooks]: new Hooks(),
[kSchemaController]: schemaController,
[kSchemaErrorFormatter]: null,
[kErrorHandler]: defaultErrorHandler,
[kErrorHandler]: buildErrorHandler(),
[kReplySerializerDefault]: null,
[kContentTypeParser]: new ContentTypeParser(
bodyLimit,
Expand Down Expand Up @@ -338,7 +323,7 @@ function fastify (options) {
},
errorHandler: {
get () {
return this[kErrorHandler]
return this[kErrorHandler].func
}
}
})
Expand Down Expand Up @@ -654,7 +639,7 @@ function fastify (options) {
function setErrorHandler (func) {
throwIfAlreadyStarted('Cannot call "setErrorHandler" when fastify instance is already started!')

this[kErrorHandler] = func.bind(this)
this[kErrorHandler] = buildErrorHandler(this[kErrorHandler], func.bind(this))
return this
}

Expand Down
153 changes: 153 additions & 0 deletions lib/error-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
'use strict'

const FJS = require('fast-json-stringify')
const statusCodes = require('http').STATUS_CODES
const wrapThenable = require('./wrapThenable')
const {
kReplyHeaders, kReplyNextErrorHandler, kReplyIsRunningOnErrorHook, kReplySent, kReplyHasStatusCode
} = require('./symbols.js')

const { getSchemaSerializer } = require('./schemas')

const serializeError = FJS({
type: 'object',
properties: {
statusCode: { type: 'number' },
code: { type: 'string' },
error: { type: 'string' },
message: { type: 'string' }
}
})

const rootErrorHandler = {
func: defaultErrorHandler,
toJSON () {
return this.func.name.toString() + '()'
}
}

function handleError (reply, error, cb) {
reply[kReplyIsRunningOnErrorHook] = false

const context = reply.context
if (reply[kReplyNextErrorHandler] === false) {
fallbackErrorHandler(error, reply, function (reply, payload) {
reply.raw.writeHead(reply.raw.statusCode, reply[kReplyHeaders])
reply.raw.end(payload)
})
return
}
const errorHandler = reply[kReplyNextErrorHandler] || context.errorHandler

// In case the error handler throws, we set the next errorHandler so we can error again
reply[kReplyNextErrorHandler] = Object.getPrototypeOf(errorHandler)

reply[kReplyHeaders]['content-length'] = undefined

const func = errorHandler.func

if (!func) {
reply[kReplyNextErrorHandler] = false
fallbackErrorHandler(error, reply, cb)
return
}

const result = func(error, reply.request, reply)
if (result !== undefined) {
if (result !== null && typeof result.then === 'function') {
wrapThenable(result, reply)
} else {
reply.send(result)
}
}
}

function defaultErrorHandler (error, request, reply) {
setErrorHeaders(error, reply)
if (!reply[kReplyHasStatusCode] || reply.statusCode === 200) {
const statusCode = error.statusCode || error.status
reply.code(statusCode >= 400 ? statusCode : 500)
}
if (reply.statusCode < 500) {
reply.log.info(
{ res: reply, err: error },
error && error.message
)
} else {
reply.log.error(
{ req: request, res: reply, err: error },
error && error.message
)
}
reply.send(error)
}

function fallbackErrorHandler (error, reply, cb) {
const res = reply.raw
const statusCode = reply.statusCode
let payload
try {
const serializerFn = getSchemaSerializer(reply.context, statusCode)
payload = (serializerFn === false)
? serializeError({
error: statusCodes[statusCode + ''],
code: error.code,
message: error.message,
statusCode: statusCode
})
: serializerFn(Object.create(error, {
error: { value: statusCodes[statusCode + ''] },
message: { value: error.message },
statusCode: { value: statusCode }
}))
} catch (err) {
// error is always FST_ERR_SCH_SERIALIZATION_BUILD because this is called from route/compileSchemasForSerialization
reply.log.error({ err, statusCode: res.statusCode }, 'The serializer for the given status code failed')
reply.code(500)
payload = serializeError({
error: statusCodes['500'],
message: err.message,
statusCode: 500
})
}

reply[kReplyHeaders]['content-type'] = 'application/json; charset=utf-8'
reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
kibertoad marked this conversation as resolved.
Show resolved Hide resolved

reply[kReplySent] = true

cb(reply, payload)
}

function buildErrorHandler (parent = rootErrorHandler, func) {
if (!func) {
return parent
}

const errorHandler = Object.create(parent)
errorHandler.func = func
return errorHandler
}

function setErrorHeaders (error, reply) {
const res = reply.raw
let statusCode = res.statusCode
statusCode = (statusCode >= 400) ? statusCode : 500
// treat undefined and null as same
if (error != null) {
if (error.headers !== undefined) {
reply.headers(error.headers)
}
if (error.status >= 400) {
statusCode = error.status
} else if (error.statusCode >= 400) {
statusCode = error.statusCode
}
}
res.statusCode = statusCode
}

module.exports = {
buildErrorHandler,
handleError
}
4 changes: 3 additions & 1 deletion lib/fourOhFour.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ const {
kErrorHandler
} = require('./symbols.js')
const { lifecycleHooks } = require('./hooks')
const { buildErrorHandler } = require('./error-handler.js')
const fourOhFourContext = {
config: {
},
onSend: [],
onError: []
onError: [],
errorHandler: buildErrorHandler()
}

/**
Expand Down