diff --git a/docs/api/index.md b/docs/api/index.md index b30e3aca5..1b1773307 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -212,6 +212,68 @@ Note: - Many properties on `ctx` are defined using getters, setters, and `Object.defineProperty()`. You can only edit these properties (not recommended) by using `Object.defineProperty()` on `app.context`. See https://github.com/koajs/koa/issues/652. - Mounted apps currently use their parent's `ctx` and settings. Thus, mounted apps are really just groups of middleware. +## Events + + Koa application is an instance of `EventEmitter` and emits following events to allow hooks into lifecycle. + All these events (except `error`) have `ctx` as first parameter: + +### Event: 'request' + + Emitted each time there is a request, with `ctx` as parameter, before passing to any middleware. + May be a good place for per-request context mutation, when needed, for example: + + ```js + app.on('request', ctx => { + ctx.state.start = Date.now(); + // or something more advanced + if(!ctx.get('DNT')) ctx.state = new Proxy({}) + }) + ``` + +### Event: 'respond' + + Emitted after passing all middleware, but before sending the response to network. + May be used when some action required to be latest after all middleware processing, for example: + + ```js + app.on('respond', ctx => { + if (ctx.state.start) ctx.set('X-Response-Time', Date.now() - ctx.state.start) + }) + ``` + + Note: `respond` event may not be emitted in case of premature socket close (due to a middleware timeout, for example). + +### Event: 'responded' + + Emitted when the response stream is finished. Good place to cleanup any resources attached to `ctx.state` for example: + + ```js + app.on('responded', ctx => { + if (ctx.state.dataStream) ctx.state.dataStream.destroy(); + }) + ``` + + More advanced example, use events to detect that server is idling for some time: + + ```js + const server = app.listen(); + const IDLE_INTERVAL = 2 * server.timeout; + const onIdle = () => { console.warn(`Server is idle for ${IDLE_INTERVAL / 1000} seconds!`); } + let idleInterval = setInterval(onIdle, IDLE_INTERVAL); + app + .on('request', () => { + clearInterval(idleInterval); + }) + .on('responded', () => { + clearInterval(idleInterval); + idleInterval = setInterval(onIdle, IDLE_INTERVAL); + }) + ``` + +### Event: 'error' + + See **Error Handling** below. + ## Error Handling By default outputs all errors to stderr unless `app.silent` is `true`. diff --git a/lib/application.js b/lib/application.js index e01019796..7cbe50faf 100644 --- a/lib/application.js +++ b/lib/application.js @@ -162,9 +162,23 @@ module.exports = class Application extends Emitter { handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; - const onerror = err => ctx.onerror(err); - const handleResponse = () => respond(ctx); + + const responding = Symbol('responding'); + this.once(responding, () => this.emit('respond', ctx)); + + const onerror = err => { + if (null != err) { + ctx.onerror(err); + this.emit(responding); + } + }; + const handleResponse = () => { + this.emit(responding); + respond(ctx); + }; + this.emit('request', ctx); onFinished(res, onerror); + onFinished(ctx.socket, () => { this.emit('responded', ctx); }); return fnMiddleware(ctx).then(handleResponse).catch(onerror); } diff --git a/test/application/events.js b/test/application/events.js new file mode 100644 index 000000000..ffa1fd69b --- /dev/null +++ b/test/application/events.js @@ -0,0 +1,92 @@ + +'use strict'; + +const assert = require('assert'); +const Koa = require('../..'); +const request = require('supertest'); + +describe('app emits events', () => { + it('should emit request, respond and responded once and in correct order', async() => { + const app = new Koa(); + const emitted = []; + ['request', 'respond', 'responded', 'error', 'customEvent'].forEach(event => app.on(event, () => emitted.push(event))); + + app.use((ctx, next) => { + emitted.push('fistMiddleWare'); + ctx.body = 'hello!'; + return next(); + }); + + app.use((ctx, next) => { + ctx.app.emit('customEvent'); + return next(); + }); + + app.use(ctx => { + emitted.push('lastMiddleware'); + }); + + const server = app.listen(); + + let onceEvents = 0; + ['request', 'respond', 'responded'].forEach(event => + app.once(event, ctx => { + assert.strictEqual(ctx.app, app); + onceEvents++; + }) + ); + + await request(server) + .get('/') + .expect(200); + + assert.deepStrictEqual(emitted, ['request', 'fistMiddleWare', 'customEvent', 'lastMiddleware', 'respond', 'responded']); + assert.strictEqual(onceEvents, 3); + }); + + it('should emit error event on middleware throw', async() => { + const app = new Koa(); + const emitted = []; + ['request', 'respond', 'responded', 'error'].forEach(event => app.on(event, () => emitted.push(event))); + + app.use((ctx, next) => { + throw new TypeError('Hello Koa!'); + }); + + const server = app.listen(); + + let onceEvents = 0; + app.once('error', (err, ctx) => { + assert.ok(err instanceof TypeError); + assert.strictEqual(ctx.app, app); + onceEvents++; + }); + + await request(server) + .get('/') + .expect(500); + + assert.deepStrictEqual(emitted, ['request', 'error', 'respond', 'responded']); + assert.strictEqual(onceEvents, 1); + }); + + it('should emit correct events on middleware timeout', async() => { + const app = new Koa(); + const emitted = []; + ['request', 'respond', 'responded', 'error', 'timeout'].forEach(event => app.on(event, () => emitted.push(event))); + + app.use(async(ctx, next) => { + await new Promise(resolve => setTimeout(resolve, 2000)); + ctx.body = 'Timeout'; + }); + + const server = app.listen(); + server.setTimeout(1000, socket => { app.emit('timeout'); socket.end('HTTP/1.1 408 Request Timeout\r\n\r\n'); }); + + await request(server) + .get('/') + .expect(408); + + assert.deepStrictEqual(emitted, ['request', 'responded', 'timeout']); + }); +});