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 8 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
17 changes: 12 additions & 5 deletions docs/Errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,16 @@ 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.
If you have defined a custom error handler using `setErrorHandler` the error will
be routed there. If `setErrorHandler` has been called multiple times, the error will be
routed to the precedent error handler if the error handler or an hook `throw` or
call `reply.send(error)`.
Error handlers are fully encapsulated, so a `setErrorHandler` call in an encapsulated
plugin will limit its effect there.
mcollina marked this conversation as resolved.
Show resolved Hide resolved
The root error handler is Fastify’s generic error handler which 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.
mcollina marked this conversation as resolved.
Show resolved Hide resolved

Some things to consider in your custom error handler:

Expand All @@ -46,10 +55,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