From cbd6a311169b0cf5f591e38c039a642d0c72d938 Mon Sep 17 00:00:00 2001 From: Ryan Ling Date: Mon, 14 Sep 2020 13:40:19 +1000 Subject: [PATCH] feat(ErrorMiddleware): Support custom JSON errors (#35) Currently, other packages that want to expose a JSON response via Koala's `ErrorMiddleware` have to import Koala itself in order to use its `JsonResponse` error class. This proposes a structural approach where we rely on the object to have an `isJsonResponse` property. --- src/asyncMiddleware/asyncMiddleware.ts | 3 +-- src/errorMiddleware/README.md | 2 ++ src/errorMiddleware/errorMiddleware.test.ts | 11 +++++++++++ src/errorMiddleware/errorMiddleware.ts | 18 ++++++++++++------ src/requestLogging/requestLogging.ts | 7 ++++--- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/asyncMiddleware/asyncMiddleware.ts b/src/asyncMiddleware/asyncMiddleware.ts index f8487d9..b229f6a 100644 --- a/src/asyncMiddleware/asyncMiddleware.ts +++ b/src/asyncMiddleware/asyncMiddleware.ts @@ -24,10 +24,9 @@ export const lazyLoad = ( const cacheInit = await init(); cacheTimestamp = Date.now(); return cacheInit; - } catch (err) { + } catch (err: unknown) { cache = undefined; - /* eslint-disable-next-line no-throw-literal */ throw err; } }; diff --git a/src/errorMiddleware/README.md b/src/errorMiddleware/README.md index 85e3eb3..d9562e9 100644 --- a/src/errorMiddleware/README.md +++ b/src/errorMiddleware/README.md @@ -54,3 +54,5 @@ ctx.throw( }), ); ``` + +You can also bring your own child Error class by exposing an `isJsonResponse` property set to `true`. diff --git a/src/errorMiddleware/errorMiddleware.test.ts b/src/errorMiddleware/errorMiddleware.test.ts index 5659018..c9cc10e 100644 --- a/src/errorMiddleware/errorMiddleware.test.ts +++ b/src/errorMiddleware/errorMiddleware.test.ts @@ -90,6 +90,17 @@ describe('errorMiddleware', () => { .expect(403, { access: false }); }); + it('exposes a thrown 4xx custom JSON response as JSON based on `Accept`', async () => { + mockNext.mockImplementation((ctx) => { + ctx.throw(403, { body: { access: false }, isJsonResponse: true }); + }); + + await agent() + .get('/') + .set('Accept', 'application/json') + .expect(403, { access: false }); + }); + it('exposes a thrown 4xx `JsonResponse` as text based on `Accept`', async () => { mockNext.mockImplementation((ctx) => { ctx.throw(410, new JsonResponse('Gone away', { gone: true })); diff --git a/src/errorMiddleware/errorMiddleware.ts b/src/errorMiddleware/errorMiddleware.ts index cd43928..5e0ef31 100644 --- a/src/errorMiddleware/errorMiddleware.ts +++ b/src/errorMiddleware/errorMiddleware.ts @@ -19,6 +19,12 @@ const isObject = (value: unknown): value is Record => * ``` */ export class JsonResponse extends Error { + /** + * The property used by `handle` to infer that this error contains a body that + * can be exposed in the HTTP response. + */ + public isJsonResponse = true as const; + /** * Creates a new `JsonResponse` * @@ -45,9 +51,9 @@ export class JsonResponse extends Error { * status, and will set the error message as the response body for non-5xx * statuses. It works well with Koa's built-in `ctx.throw`. * - * This includes a specific check for the `JsonResponse` class to support - * including a JSON response body. If the request accepts `application/json` - * the error's `body` will be returned, otherwise its plain text `message`. + * This includes support for a JSON response body by throwing an error with + * `isJsonResponse` set to `true`. If the request accepts `application/json` the + * error's `body` will be returned, otherwise its plain text `message`. * * This should be placed high up the middleware chain so that errors from lower * middleware are handled. It also serves to set the correct `ctx.status` for @@ -65,8 +71,8 @@ export class JsonResponse extends Error { export const handle: Middleware = async (ctx, next) => { try { return (await next()) as unknown; - } catch (err) { - ctx.state[ERROR_STATE_KEY] = err as unknown; + } catch (err: unknown) { + ctx.state[ERROR_STATE_KEY] = err; if (!isObject(err) || typeof err.status !== 'number') { ctx.status = 500; @@ -79,7 +85,7 @@ export const handle: Middleware = async (ctx, next) => { if ( expose && - err instanceof JsonResponse && + err.isJsonResponse === true && // Prefer JSON ourselves if the request has no preference ctx.accepts(['application/json', 'text/plain']) === 'application/json' ) { diff --git a/src/requestLogging/requestLogging.ts b/src/requestLogging/requestLogging.ts index bd04d81..1440b97 100644 --- a/src/requestLogging/requestLogging.ts +++ b/src/requestLogging/requestLogging.ts @@ -132,8 +132,9 @@ export const createMiddleware = ( try { await next(); requestFinished({ status: ctx.response.status }); - } catch (e) { - requestFinished({ status: 500, internalErrorString: String(e) }, e); - throw e; + } catch (err: unknown) { + requestFinished({ status: 500, internalErrorString: String(err) }, err); + + throw err; } };