-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ErrorMiddleware): Add module (#9)
- Loading branch information
Showing
9 changed files
with
317 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# 🐨 Error Middleware 🐨 | ||
|
||
## Introduction | ||
|
||
Catches errors thrown from downstream middleware, as specified here: | ||
|
||
<https://github.com/koajs/koa/wiki/Error-Handling#catching-downstream-errors> | ||
|
||
This tries to extract a numeric error `status` to serve as the response 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 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 middleware that emit logs or metrics containing the response status. | ||
|
||
## Usage | ||
|
||
```typescript | ||
import { ErrorMiddleware, RequestLoggingMiddleware } from 'seek-koala'; | ||
|
||
const requestLoggingMiddleware = RequestLogging.createMiddleware( | ||
(ctx, fields, err) => { | ||
const data = { | ||
...fields, | ||
err: err ?? ErrorMiddleware.thrown(ctx), | ||
}; | ||
|
||
return ctx.status < 500 | ||
? rootLogger.info(data, 'request') | ||
: rootLogger.error(data, 'request'); | ||
}, | ||
); | ||
|
||
app | ||
.use(requestLoggingMiddleware) | ||
.use(metricsMiddleware) | ||
.use(ErrorMiddleware.handle); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import Koa from 'koa'; | ||
|
||
import { agentFromApp } from '../testing/server'; | ||
|
||
import { handle, thrown } from './errorMiddleware'; | ||
|
||
describe('errorMiddleware', () => { | ||
const mockPrev = jest.fn<unknown, [Koa.Context, Koa.Next]>(); | ||
const mockNext = jest.fn<unknown, [Koa.Context, Koa.Next]>(); | ||
|
||
const app = new Koa().use(mockPrev).use(handle).use(mockNext); | ||
|
||
const agent = agentFromApp(app); | ||
|
||
beforeAll(agent.setup); | ||
|
||
beforeEach(() => mockPrev.mockImplementation((_, next) => next())); | ||
|
||
afterEach(mockPrev.mockReset); | ||
afterEach(mockNext.mockReset); | ||
|
||
afterAll(agent.teardown); | ||
|
||
it('passes through a returned 2xx', async () => { | ||
mockNext.mockImplementation((ctx) => { | ||
ctx.status = 200; | ||
ctx.body = 'good'; | ||
}); | ||
|
||
await agent().get('/').expect(200, 'good'); | ||
}); | ||
|
||
it('passes through a returned 5xx', async () => { | ||
mockNext.mockImplementation((ctx) => { | ||
ctx.status = 500; | ||
ctx.body = 'evil'; | ||
}); | ||
|
||
await agent().get('/').expect(500, 'evil'); | ||
}); | ||
|
||
it('provides thrown error to higher middleware', async () => { | ||
expect.assertions(1); | ||
|
||
mockPrev.mockImplementation(async (ctx, next) => { | ||
await next(); | ||
|
||
expect(thrown(ctx)).toEqual(expect.any(Error)); | ||
}); | ||
|
||
mockNext.mockImplementation((ctx) => { | ||
ctx.throw(400, 'bad'); | ||
}); | ||
|
||
await agent().get('/').expect(400, 'bad'); | ||
}); | ||
|
||
it('exposes a thrown 4xx error', async () => { | ||
mockNext.mockImplementation((ctx) => { | ||
ctx.throw(400, 'bad'); | ||
}); | ||
|
||
await agent().get('/').expect(400, 'bad'); | ||
}); | ||
|
||
it('redacts a thrown 5xx error', async () => { | ||
mockNext.mockImplementation((ctx) => { | ||
ctx.throw(500, 'bad'); | ||
}); | ||
|
||
await agent().get('/').expect(500, ''); | ||
}); | ||
|
||
it('handles directly-thrown error', async () => { | ||
mockNext.mockImplementation(() => { | ||
throw new Error('bad'); | ||
}); | ||
|
||
await agent().get('/').expect(500, ''); | ||
}); | ||
|
||
it('handles null error', async () => { | ||
mockNext.mockImplementation(() => { | ||
/* eslint-disable-next-line no-throw-literal */ | ||
throw null; | ||
}); | ||
|
||
await agent().get('/').expect(500, ''); | ||
}); | ||
|
||
it('handles string error', async () => { | ||
mockNext.mockImplementation(() => { | ||
/* eslint-disable-next-line no-throw-literal */ | ||
throw 'bad'; | ||
}); | ||
|
||
await agent().get('/').expect(500, ''); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { Context, Middleware } from 'koa'; | ||
|
||
/** | ||
* @see {@link https://github.com/microsoft/TypeScript/issues/1863} | ||
*/ | ||
const ERROR_STATE_KEY = (Symbol('seek-koala-error') as unknown) as string; | ||
|
||
const isObject = (value: unknown): value is Record<PropertyKey, unknown> => | ||
typeof value === 'object' && value !== null; | ||
|
||
/** | ||
* Catches errors thrown from downstream middleware, as specified here: | ||
* | ||
* https://github.com/koajs/koa/wiki/Error-Handling#catching-downstream-errors | ||
* | ||
* This tries to extract a numeric error `status` to serve as the response | ||
* 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 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 | ||
* middleware that emit logs or metrics containing the response status. For | ||
* this reason, we recommend a sequence like: | ||
* | ||
* ```javascript | ||
* app | ||
* .use(requestLoggingMiddleware) | ||
* .use(metricsMiddleware) | ||
* .use(ErrorMiddleware.handle) | ||
* .use(allTheRest); | ||
* ``` | ||
*/ | ||
export const handle: Middleware = async (ctx, next) => { | ||
try { | ||
return (await next()) as unknown; | ||
} catch (err) { | ||
ctx.state[ERROR_STATE_KEY] = err as unknown; | ||
|
||
if (!isObject(err) || typeof err.status !== 'number') { | ||
ctx.status = 500; | ||
ctx.body = ''; | ||
return; | ||
} | ||
|
||
ctx.status = err.status; | ||
ctx.body = (err.status < 500 && err.message) || ''; | ||
} | ||
}; | ||
|
||
/** | ||
* Retrieve the error caught by `ErrorMiddleware.handle` from state. | ||
* | ||
* This is useful if you want to do something with the error higher up in the | ||
* middleware chain. | ||
*/ | ||
export const thrown = (ctx: Context): unknown => | ||
ctx.state[ERROR_STATE_KEY] as unknown; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Server } from 'http'; | ||
|
||
import Koa from 'koa'; | ||
import request from 'supertest'; | ||
|
||
/** | ||
* Create a new SuperTest agent from a Koa application. | ||
*/ | ||
export const agentFromApp = <State, Context>(app: Koa<State, Context>) => { | ||
let server: Server; | ||
let agent: request.SuperTest<request.Test>; | ||
|
||
const getAgent = () => agent; | ||
|
||
const setup = async () => { | ||
await new Promise((resolve) => (server = app.listen(undefined, resolve))); | ||
agent = request.agent(server); | ||
}; | ||
|
||
const teardown = () => new Promise((resolve) => server.close(resolve)); | ||
|
||
return Object.assign(getAgent, { setup, teardown }); | ||
}; |
Oops, something went wrong.