Skip to content

Commit

Permalink
feat(ErrorMiddleware): Support custom JSON errors (#35)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
72636c authored Sep 14, 2020
1 parent 78a411f commit cbd6a31
Show file tree
Hide file tree
Showing 5 changed files with 30 additions and 11 deletions.
3 changes: 1 addition & 2 deletions src/asyncMiddleware/asyncMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ export const lazyLoad = <State, Context>(
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;
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/errorMiddleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,5 @@ ctx.throw(
}),
);
```

You can also bring your own child Error class by exposing an `isJsonResponse` property set to `true`.
11 changes: 11 additions & 0 deletions src/errorMiddleware/errorMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand Down
18 changes: 12 additions & 6 deletions src/errorMiddleware/errorMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ const isObject = (value: unknown): value is Record<PropertyKey, unknown> =>
* ```
*/
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`
*
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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'
) {
Expand Down
7 changes: 4 additions & 3 deletions src/requestLogging/requestLogging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,9 @@ export const createMiddleware = <StateT extends State, CustomT>(
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;
}
};

0 comments on commit cbd6a31

Please sign in to comment.