Skip to content

Commit

Permalink
Add tests for error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
domdomegg committed May 6, 2024
1 parent b8cbd33 commit 6c86f95
Showing 1 changed file with 202 additions and 15 deletions.
217 changes: 202 additions & 15 deletions test/encapsulated-error-handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,66 @@
const { test } = require('tap')
const Fastify = require('..')

test('encapsulates an error handler', async t => {
t.plan(3)
// Because of how error handlers wrap things, following the control flow can be tricky
// In this test file numbered comments indicate the order statements are expected to execute

test('encapsulates an asynchronous error handler', async t => {
t.plan(4)

const fastify = Fastify()
fastify.register(async function (fastify) {
fastify.setErrorHandler(async function a (err) {
t.equal(err.message, 'kaboom')
throw new Error('caught')
// 3. the inner error handler catches the error, and throws a new error
t.equal(err.message, 'from_endpoint')
throw new Error('from_inner')
})
fastify.get('/encapsulated', async () => {
// 2. the endpoint throws an error
throw new Error('from_endpoint')
})
})

fastify.setErrorHandler(async function b (err) {
// 4. the outer error handler catches the error thrown by the inner error handler
t.equal(err.message, 'from_inner')
// 5. the outer error handler throws a new error
throw new Error('from_outer')
})

// 1. the endpoint is called
const res = await fastify.inject('/encapsulated')
// 6. the default error handler returns the error from the outer error handler
t.equal(res.json().message, 'from_outer')
})

// See discussion in https://github.com/fastify/fastify/pull/5222#discussion_r1432573655
test('encapsulates a synchronous error handler', async t => {
t.plan(4)

const fastify = Fastify()
fastify.register(async function (fastify) {
fastify.setErrorHandler(function a (err) {
// 3. the inner error handler catches the error, and throws a new error
t.equal(err.message, 'from_endpoint')
throw new Error('from_inner')
})
fastify.get('/encapsulated', async () => {
// 2. the endpoint throws an error
throw new Error('from_endpoint')
})
fastify.get('/encapsulated', async () => { throw new Error('kaboom') })
})

fastify.setErrorHandler(async function b (err) {
t.equal(err.message, 'caught')
throw new Error('wrapped')
// 4. the outer error handler catches the error thrown by the inner error handler
t.equal(err.message, 'from_inner')
// 5. the outer error handler throws a new error
throw new Error('from_outer')
})

// 1. the endpoint is called
const res = await fastify.inject('/encapsulated')
t.equal(res.json().message, 'wrapped')
// 6. the default error handler returns the error from the outer error handler
t.equal(res.json().message, 'from_outer')
})

test('onError hook nested', async t => {
Expand All @@ -30,21 +71,167 @@ test('onError hook nested', async t => {
const fastify = Fastify()
fastify.register(async function (fastify) {
fastify.setErrorHandler(async function a (err) {
t.equal(err.message, 'kaboom')
throw new Error('caught')
// 4. the inner error handler catches the error, and throws a new error
t.equal(err.message, 'from_endpoint')
throw new Error('from_inner')
})
fastify.get('/encapsulated', async () => {
// 2. the endpoint throws an error
throw new Error('from_endpoint')
})
fastify.get('/encapsulated', async () => { throw new Error('kaboom') })
})

fastify.setErrorHandler(async function b (err) {
t.equal(err.message, 'caught')
throw new Error('wrapped')
// 5. the outer error handler catches the error thrown by the inner error handler
t.equal(err.message, 'from_inner')
// 6. the outer error handler throws a new error
throw new Error('from_outer')
})

fastify.addHook('onError', async function (request, reply, err) {
t.equal(err.message, 'kaboom')
// 3. the hook receives the error
t.equal(err.message, 'from_endpoint')
})

// 1. the endpoint is called
const res = await fastify.inject('/encapsulated')
// 7. the default error handler returns the error from the outer error handler
t.equal(res.json().message, 'from_outer')
})

// See https://github.com/fastify/fastify/issues/5220
test('encapuslates an error handler, for errors thrown in hooks', async t => {
t.plan(3)

const fastify = Fastify()
fastify.register(async function (fastify) {
fastify.setErrorHandler(function a (err) {
// 3. the inner error handler catches the error, and throws a new error
t.equal(err.message, 'from_hook')
throw new Error('from_inner')
})
fastify.addHook('onRequest', async () => {
// 2. the hook throws an error
throw new Error('from_hook')
})
fastify.get('/encapsulated', async () => {})
})

fastify.setErrorHandler(function b (err) {
// 4. the outer error handler catches the error thrown by the inner error handler
t.equal(err.message, 'from_inner')
// 5. the outer error handler throws a new error
throw new Error('from_outer')
})

// 1. the endpoint is called
const res = await fastify.inject('/encapsulated')
// 6. the default error handler returns the error from the outer error handler
t.equal(res.json().message, 'from_outer')
})

// See https://github.com/fastify/fastify/issues/5220
test('encapuslates many synchronous error handlers that rethrow errors', async t => {
const DEPTH = 100
t.plan(DEPTH + 2)

/**
* This creates a very nested set of error handlers, that looks like:
* plugin
* - error handler
* - plugin
* - error handler
* - plugin
* ... {to DEPTH levels}
* - plugin
* - error handler
* - GET /encapsulated
*/
const createNestedRoutes = (fastify, depth) => {
if (depth < 0) {
throw new Error('Expected depth >= 0')
} else if (depth === 0) {
fastify.setErrorHandler(function a (err) {
// 3. innermost error handler catches the error, and throws a new error
t.equal(err.message, 'from_route')
throw new Error(`from_handler_${depth}`)
})
fastify.get('/encapsulated', async () => {
// 2. the endpoint throws an error
throw new Error('from_route')
})
} else {
fastify.setErrorHandler(function d (err) {
// 4 to {DEPTH+4}. error handlers each catch errors, and then throws a new error
t.equal(err.message, `from_handler_${depth - 1}`)
throw new Error(`from_handler_${depth}`)
})

fastify.register(async function (fastify) {
createNestedRoutes(fastify, depth - 1)
})
}
}

const fastify = Fastify()
createNestedRoutes(fastify, DEPTH)

// 1. the endpoint is called
const res = await fastify.inject('/encapsulated')
// {DEPTH+5}. the default error handler returns the error from the outermost error handler
t.equal(res.json().message, `from_handler_${DEPTH}`)
})

// See https://github.com/fastify/fastify/issues/5220
// This was not failing previously, but we want to make sure the behavior continues to work in the same way across async and sync handlers
// Plus, the current setup is somewhat fragile to tweaks to wrapThenable as that's what retries (by calling res.send(err) again)
test('encapuslates many asynchronous error handlers that rethrow errors', async t => {
const DEPTH = 100
t.plan(DEPTH + 2)

/**
* This creates a very nested set of error handlers, that looks like:
* plugin
* - error handler
* - plugin
* - error handler
* - plugin
* ... {to DEPTH levels}
* - plugin
* - error handler
* - GET /encapsulated
*/
const createNestedRoutes = (fastify, depth) => {
if (depth < 0) {
throw new Error('Expected depth >= 0')
} else if (depth === 0) {
fastify.setErrorHandler(async function a (err) {
// 3. innermost error handler catches the error, and throws a new error
t.equal(err.message, 'from_route')
throw new Error(`from_handler_${depth}`)
})
fastify.get('/encapsulated', async () => {
// 2. the endpoint throws an error
throw new Error('from_route')
})
} else {
fastify.setErrorHandler(async function m (err) {
// 4 to {DEPTH+4}. error handlers each catch errors, and then throws a new error
t.equal(err.message, `from_handler_${depth - 1}`)
throw new Error(`from_handler_${depth}`)
})

fastify.register(async function (fastify) {
createNestedRoutes(fastify, depth - 1)
})
}
}

const fastify = Fastify()
createNestedRoutes(fastify, DEPTH)

// 1. the endpoint is called
const res = await fastify.inject('/encapsulated')
t.equal(res.json().message, 'wrapped')
// {DEPTH+5}. the default error handler returns the error from the outermost error handler
t.equal(res.json().message, `from_handler_${DEPTH}`)
})

0 comments on commit 6c86f95

Please sign in to comment.