From 3705b5fb4c88c55d69531d9df9f4601581e25c6c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 30 Dec 2022 16:20:49 +0530 Subject: [PATCH] feat: add support for custom exception handling --- README.md | 46 ++++++++++++++ src/runner.ts | 40 +++++++++++- src/types.ts | 15 ++++- tests/runner.spec.ts | 148 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 240 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e29c253..253eeb5 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,52 @@ await middleware assert.deepEqual(context.stack, ['fn1', 'final handler']) ``` +### Error handler +By default, the exceptions raised in the middleware pipeline are bubbled upto the `run` method and you can capture them using `try/catch` block. Also, when an exception is raised, the middleware downstream logic will not run, unless middleware internally wraps the `next` method call inside `try/catch` block. + +To simply the exception handling process, you can define a custom error handler to catch the exceptions and resume the downstream flow of middleware. + +```ts +const context = { + stack: [], +} +const middleware = new Middleware() + +middleware.add((ctx: typeof context, next: NextFn) => { + ctx.stack.push('middleware 1 upstream') + await next() + ctx.stack.push('middleware 1 downstream') +}) + +middleware.add((ctx: typeof context, next: NextFn) => { + ctx.stack.push('middleware 2 upstream') + throw new Error('Something went wrong') +}) + +middleware.add((ctx: typeof context, next: NextFn) => { + ctx.stack.push('middleware 3 upstream') + await next() + ctx.stack.push('middleware 3 downstream') +}) + +await middleware + .runner() + .errorHandler((error) => { + console.log(error) + context.stack.push('error handler') + }) + .finalHandler(() => { + context.stack.push('final handler') + }) + .run((fn, next) => fn(context, next)) + +assert.deepEqual(context.stack, [ + 'middleware 1 upstream', + 'middleware 2 upstream', + 'middleware 1 downstream' +]) +``` + [gh-workflow-image]: https://img.shields.io/github/workflow/status/poppinss/middleware/test?style=for-the-badge [gh-workflow-url]: https://github.com/poppinss/middleware/actions/workflows/test.yml "Github action" diff --git a/src/runner.ts b/src/runner.ts index 7619a83..e2687ff 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import type { Executor, FinalHandler } from './types.js' +import type { ErrorHandler, Executor, FinalHandler } from './types.js' /** * Run a function only once. Tightly coupled with the Runner class @@ -61,6 +61,11 @@ export class Runner { */ #finalHandler: FinalHandler = DEFAULT_FINAL_HANDLER + /** + * Error handler to self handle errors + */ + #errorHandler?: ErrorHandler + constructor(middleware: MiddlewareFn[]) { this.#middleware = middleware } @@ -85,6 +90,24 @@ export class Runner { return self.#executor(middleware, once(self, self.#invoke)) } + /** + * Same as invoke, but captures errors + */ + #invokeWithErrorManagement(self: Runner): Promise | void { + const middleware = self.#middleware[self.#currentIndex++] + + /** + * Empty stack + */ + if (!middleware) { + return self.#finalHandler().catch(self.#errorHandler) + } + + return self + .#executor(middleware, once(self, self.#invokeWithErrorManagement)) + .catch(self.#errorHandler) + } + /** * Final handler to be executed, when the chain ends successfully. */ @@ -93,12 +116,27 @@ export class Runner { return this } + /** + * Specify a custom error handler to use. Defining an error handler + * turns will make run method not throw an exception and instead + * run the upstream middleware logic + */ + errorHandler(errorHandler: ErrorHandler): this { + this.#errorHandler = errorHandler + return this + } + /** * Start the middleware queue and pass params to it. The `params` * array will be passed as spread arguments. */ async run(cb: Executor): Promise { this.#executor = cb + + if (this.#errorHandler) { + return this.#invokeWithErrorManagement(this) + } + return this.#invoke(this) } } diff --git a/src/types.ts b/src/types.ts index 1f10181..492e221 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,15 +7,24 @@ * file that was distributed with this source code. */ -export type NextFn = () => Promise | void +export type NextFn = () => Promise | any /** * Final handler is called when the entire chain has been * executed successfully. */ -export type FinalHandler = () => Promise | void +export type FinalHandler = () => Promise + +/** + * Error handler is called any method in the pipeline raises + * an exception + */ +export type ErrorHandler = (error: any) => Promise /** * The executor function that invokes the middleware */ -export type Executor = (middleware: MiddlewareFn, next: NextFn) => any +export type Executor = ( + middleware: MiddlewareFn, + next: NextFn +) => Promise diff --git a/tests/runner.spec.ts b/tests/runner.spec.ts index 0b54360..4d7ebfe 100644 --- a/tests/runner.spec.ts +++ b/tests/runner.spec.ts @@ -129,7 +129,7 @@ test.group('Runner', () => { }, ]) - await runner.run((fn, next) => fn.handle({}, next)) + await runner.run(async (fn, next) => fn.handle({}, next)) assert.equal(chain[0], null) assert.instanceOf(chain[1], Foo) assert.equal(chain[0], null) @@ -233,7 +233,7 @@ test.group('Runner', () => { const runner = new Runner([first]) - await runner.run((fn) => fn()) + await runner.run(async (fn) => fn()) assert.deepEqual(chain, ['first']) }) @@ -364,7 +364,7 @@ test.group('Runner', () => { } runner.finalHandler(() => finalHandler('foo')) - await runner.run((fn, next) => fn('foo', next)) + await runner.run(async (fn, next) => fn('foo', next)) assert.deepEqual(stack, ['foo', 'foo']) }) @@ -383,7 +383,7 @@ test.group('Runner', () => { } runner.finalHandler(() => finalHandler('bar')) - await runner.run((fn, next) => fn('bar', next)) + await runner.run(async (fn, next) => fn('bar', next)) assert.deepEqual(stack, ['bar']) }) @@ -405,7 +405,145 @@ test.group('Runner', () => { runner.finalHandler(() => finalHandler('bar')) - await assert.rejects(() => runner.run((fn, next) => fn('bar', next)), 'Failed') + await assert.rejects(() => runner.run(async (fn, next) => fn('bar', next)), 'Failed') assert.deepEqual(stack, ['bar']) }) + + test('hand over exception a custom exception handler', async ({ assert }) => { + assert.plan(2) + const chain: string[] = [] + + async function first(_: any, next: NextFn) { + chain.push('first') + await next() + } + + async function second() { + throw new Error('I am killed') + } + + async function third(_: any, next: NextFn) { + chain.push('third') + await next() + } + + const runner = new Runner([first, second, third]) + runner.errorHandler(async (error) => { + assert.equal(error.message, 'I am killed') + }) + + await runner.run((fn, next) => fn({}, next)) + assert.deepEqual(chain, ['first']) + }) + + test('execute middleware in reverse when error handler is defined', async ({ assert }) => { + const chain: string[] = [] + + async function first(_: any, next: NextFn) { + chain.push('first') + await next() + chain.push('first after') + } + + async function second(_: any, next: NextFn) { + chain.push('second') + await sleep(200) + await next() + await sleep(100) + chain.push('second after') + } + + async function third() { + throw new Error('Something went wrong') + } + + const runner = new Runner([first, second, third]) + runner.errorHandler(async () => {}) + + await runner.run((fn, next) => fn({}, next)) + assert.deepEqual(chain, ['first', 'second', 'second after', 'first after']) + }) + + test('return error handler response via next method', async ({ assert }) => { + const chain: string[] = [] + + async function first(_: any, next: NextFn) { + chain.push('first') + const response = await next() + chain.push('first after') + + return response + } + + async function second(_: any, next: NextFn) { + chain.push('second') + await sleep(200) + const response = await next() + await sleep(100) + chain.push('second after') + + return response + } + + async function third() { + throw new Error('Something went wrong') + } + + async function fourth() { + chain.push('fourth') + } + + const runner = new Runner([first, second, third, fourth]) + runner.errorHandler(async () => { + return 'handled' + }) + runner.finalHandler(async () => { + chain.push('final handler') + }) + + const response = await runner.run((fn, next) => fn({}, next)) + assert.equal(response, 'handled') + assert.deepEqual(chain, ['first', 'second', 'second after', 'first after']) + }) + + test('raise exception thrown by error handler', async ({ assert }) => { + const chain: string[] = [] + + async function first(_: any, next: NextFn) { + chain.push('first') + const response = await next() + chain.push('first after') + + return response + } + + async function second(_: any, next: NextFn) { + chain.push('second') + await sleep(200) + const response = await next() + await sleep(100) + chain.push('second after') + + return response + } + + async function third() { + throw new Error('Something went wrong') + } + + async function fourth() { + chain.push('fourth') + } + + const runner = new Runner([first, second, third, fourth]) + runner.errorHandler(async (error) => { + throw error + }) + runner.finalHandler(async () => { + chain.push('final handler') + }) + + await assert.rejects(() => runner.run((fn, next) => fn({}, next)), 'Something went wrong') + assert.deepEqual(chain, ['first', 'second']) + }) })