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 6 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
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
151 changes: 151 additions & 0 deletions lib/error-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
'use strict'

const FJS = require('fast-json-stringify')
const statusCodes = require('http').STATUS_CODES
const setErrorHeaders = require('./setErrorHeaders')
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) {
console.log('handleError')
reply[kReplyIsRunningOnErrorHook] = false

const context = reply.context
if (reply[kReplyNextErrorHandler] === false) {
console.log('aaa')
kibertoad marked this conversation as resolved.
Show resolved Hide resolved
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 we the error handler throws, we set the next errorHandler so we can error again
mcollina marked this conversation as resolved.
Show resolved Hide resolved
reply[kReplyNextErrorHandler] = Object.getPrototypeOf(errorHandler)

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

const func = errorHandler.func
console.log(func)
kibertoad marked this conversation as resolved.
Show resolved Hide resolved

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

console.log('calling the error handler')
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)
console.log('defaultErrorHandler', reply.statusCode, error.statusCode, reply[kReplyHasStatusCode])
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) {
console.log('fallbackErrorHandler', reply.statusCode, error.statusCode, reply[kReplyHasStatusCode])
const res = reply.raw
const statusCode = reply.statusCode
console.log(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

console.log('headerSent', res._headerSent)

console.log(cb)
cb(reply, payload)

/*
res.writeHead(statusCode, reply[kReplyHeaders])
res.end(payload)
*/
}

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

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

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
104 changes: 4 additions & 100 deletions lib/reply.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
'use strict'

const eos = require('readable-stream').finished
const statusCodes = require('http').STATUS_CODES
const FJS = require('fast-json-stringify')
const {
kSchemaResponse,
kFourOhFourContext,
kReplyErrorHandlerCalled,
kReplyHijacked,
Expand All @@ -15,25 +12,16 @@ const {
kReplyHeaders,
kReplyHasStatusCode,
kReplyIsRunningOnErrorHook,
kReplyNextErrorHandler,
kDisableRequestLogging
} = require('./symbols.js')
const { hookRunner, hookIterator, onSendHookRunner } = require('./hooks')

const internals = require('./handleRequest')[Symbol.for('internals')]
const loggerUtils = require('./logger')
const now = loggerUtils.now
const wrapThenable = require('./wrapThenable')
const setErrorHeaders = require('./setErrorHeaders')

const serializeError = FJS({
type: 'object',
properties: {
statusCode: { type: 'number' },
code: { type: 'string' },
error: { type: 'string' },
message: { type: 'string' }
}
})
const { handleError } = require('./error-handler')
const { getSchemaSerializer } = require('./schemas')

const CONTENT_TYPE = {
JSON: 'application/json; charset=utf-8',
Expand Down Expand Up @@ -508,7 +496,7 @@ function sendStream (payload, res, reply) {
}

function onErrorHook (reply, error, cb) {
if (reply.context.onError !== null && reply[kReplyErrorHandlerCalled] === true) {
if (reply.context.onError !== null && !reply[kReplyNextErrorHandler]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the coercion check necessary? Or can it be === false?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coercion is necessary. There might be a bug hiding somewhere around this check as it was one of the most troubling spot while doing this change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a comment to that effect.

reply[kReplyIsRunningOnErrorHook] = true
onSendHookRunner(
reply.context.onError,
Expand All @@ -522,65 +510,6 @@ function onErrorHook (reply, error, cb) {
}
}

function handleError (reply, error, cb) {
reply[kReplyIsRunningOnErrorHook] = false
const res = reply.raw
setErrorHeaders(error, reply)

const errorHandler = reply.context.errorHandler
if (errorHandler && reply[kReplyErrorHandlerCalled] === false) {
reply[kReplyErrorHandlerCalled] = true
reply[kReplyHeaders]['content-length'] = undefined
const result = errorHandler(error, reply.request, reply)
if (result !== undefined) {
if (result !== null && typeof result.then === 'function') {
wrapThenable(result, reply)
} else {
reply.send(result)
}
}
return
}

const statusCode = res.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')
res.statusCode = 500
payload = serializeError({
error: statusCodes['500'],
message: err.message,
statusCode: 500
})
}

reply[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)

if (cb) {
cb(reply, payload)
return
}

res.writeHead(res.statusCode, reply[kReplyHeaders])
res.end(payload)
}

function setupResponseListeners (reply) {
reply[kReplyStartTime] = now()

Expand Down Expand Up @@ -690,31 +619,6 @@ function serialize (context, data, statusCode) {
return JSON.stringify(data)
}

/**
* Search for the right JSON schema compiled function in the request context
* setup by the route configuration `schema.response`.
* It will look for the exact match (eg 200) or generic (eg 2xx)
*
* @param {object} context the request context
* @param {number} statusCode the http status code
* @returns {function|boolean} the right JSON Schema function to serialize
* the reply or false if it is not set
*/
function getSchemaSerializer (context, statusCode) {
const responseSchemaDef = context[kSchemaResponse]
if (!responseSchemaDef) {
return false
}
if (responseSchemaDef[statusCode]) {
return responseSchemaDef[statusCode]
}
const fallbackStatusCode = (statusCode + '')[0] + 'xx'
if (responseSchemaDef[fallbackStatusCode]) {
return responseSchemaDef[fallbackStatusCode]
}
return false
}

function noop () { }

module.exports = Reply
Expand Down
3 changes: 2 additions & 1 deletion lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const {
kSchemaErrorFormatter,
kErrorHandler
} = require('./symbols.js')
const { buildErrorHandler } = require('./error-handler')

function buildRouting (options) {
const router = FindMyWay(options.config)
Expand Down Expand Up @@ -246,7 +247,7 @@ function buildRouting (options) {

this.after((notHandledErr, done) => {
// Send context async
context.errorHandler = opts.errorHandler || this[kErrorHandler]
context.errorHandler = opts.errorHandler ? buildErrorHandler(this[kErrorHandler], opts.errorHandler) : this[kErrorHandler]
Eomm marked this conversation as resolved.
Show resolved Hide resolved
context._parserOptions.limit = opts.bodyLimit || null
context.logLevel = opts.logLevel
context.logSerializers = opts.logSerializers
Expand Down