From 83940d0cce1911b8caa32fd847e1e6885601ea56 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Sun, 13 Oct 2019 12:58:22 -0400 Subject: [PATCH 01/13] emit more events --- docs/api/index.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++ lib/application.js | 16 ++++++++----- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/docs/api/index.md b/docs/api/index.md index 010d4d44c..1957045c9 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -219,6 +219,64 @@ 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) + }) + ``` + +### 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 onIdle10000ms = () => { console.warn('Server is idle for 10 seconds!'); } + const IDLE_INTERVAL = 10000; + let idleInterval = setInterval(onIdle10000ms, IDLE_INTERVAL); + app + .on('request', () => { + clearInterval(idleInterval); + }) + .on('responded', () => { + idleInterval = setInterval(onIdle10000ms, 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 6415444da..96f5a757e 100644 --- a/lib/application.js +++ b/lib/application.js @@ -155,6 +155,7 @@ module.exports = class Application extends Emitter { */ handleRequest(ctx, fnMiddleware) { + this.emit('request', ctx); const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); @@ -218,18 +219,21 @@ function respond(ctx) { let body = ctx.body; const code = ctx.status; + ctx.app.emit('respond', ctx); + const onResponded = () => ctx.app.emit('responded', ctx); + // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null; - return res.end(); + return res.end(onResponded); } if ('HEAD' == ctx.method) { if (!res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body)); } - return res.end(); + return res.end(onResponded); } // status body @@ -243,12 +247,12 @@ function respond(ctx) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } - return res.end(body); + return res.end(body, onResponded); } // responses - if (Buffer.isBuffer(body)) return res.end(body); - if ('string' == typeof body) return res.end(body); + if (Buffer.isBuffer(body)) return res.end(body, onResponded); + if ('string' == typeof body) return res.end(body, onResponded); if (body instanceof Stream) return body.pipe(res); // body: json @@ -256,7 +260,7 @@ function respond(ctx) { if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } - res.end(body); + res.end(body, onResponded); } /** From 4c4d064002b0082728b8124ccba044af7fb28565 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Sun, 13 Oct 2019 18:07:07 -0400 Subject: [PATCH 02/13] handle onerror res.end --- lib/context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/context.js b/lib/context.js index 3e9575593..d62ab712b 100644 --- a/lib/context.js +++ b/lib/context.js @@ -154,7 +154,7 @@ const proto = module.exports = { const msg = err.expose ? err.message : code; this.status = err.status; this.length = Buffer.byteLength(msg); - res.end(msg); + res.end(msg, () => this.app.emit('responded', this)); }, get cookies() { From 82b5f1ceb29cfdaded216a62d56306cbfdfe2dea Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Tue, 15 Oct 2019 11:48:08 -0400 Subject: [PATCH 03/13] move all events to handleRequest --- lib/application.js | 25 +++++++++++++++---------- lib/context.js | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/application.js b/lib/application.js index 96f5a757e..ae87d8c5b 100644 --- a/lib/application.js +++ b/lib/application.js @@ -155,11 +155,17 @@ module.exports = class Application extends Emitter { */ handleRequest(ctx, fnMiddleware) { - this.emit('request', ctx); const res = ctx.res; res.statusCode = 404; - const onerror = err => ctx.onerror(err); - const handleResponse = () => respond(ctx); + const onerror = err => { + if (null != err) ctx.onerror(err); + this.emit('responded', ctx); + }; + const handleResponse = () => { + this.emit('respond', ctx); + respond(ctx); + }; + this.emit('request', ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); } @@ -220,20 +226,19 @@ function respond(ctx) { const code = ctx.status; ctx.app.emit('respond', ctx); - const onResponded = () => ctx.app.emit('responded', ctx); // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null; - return res.end(onResponded); + return res.end(); } if ('HEAD' == ctx.method) { if (!res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body)); } - return res.end(onResponded); + return res.end(); } // status body @@ -247,12 +252,12 @@ function respond(ctx) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } - return res.end(body, onResponded); + return res.end(body); } // responses - if (Buffer.isBuffer(body)) return res.end(body, onResponded); - if ('string' == typeof body) return res.end(body, onResponded); + if (Buffer.isBuffer(body)) return res.end(body); + if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res); // body: json @@ -260,7 +265,7 @@ function respond(ctx) { if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } - res.end(body, onResponded); + res.end(body); } /** diff --git a/lib/context.js b/lib/context.js index d62ab712b..3e9575593 100644 --- a/lib/context.js +++ b/lib/context.js @@ -154,7 +154,7 @@ const proto = module.exports = { const msg = err.expose ? err.message : code; this.status = err.status; this.length = Buffer.byteLength(msg); - res.end(msg, () => this.app.emit('responded', this)); + res.end(msg); }, get cookies() { From 2faaa636fccdfbec31d89b7485ae6e80409e1134 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 14:30:51 -0400 Subject: [PATCH 04/13] remove duplicated event emit --- lib/application.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/application.js b/lib/application.js index ae87d8c5b..98280fe26 100644 --- a/lib/application.js +++ b/lib/application.js @@ -225,8 +225,6 @@ function respond(ctx) { let body = ctx.body; const code = ctx.status; - ctx.app.emit('respond', ctx); - // ignore body if (statuses.empty[code]) { // strip headers From 7303f20dc9df631acd284b053da09149cff84db0 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 14:40:23 -0400 Subject: [PATCH 05/13] add test --- test/application/events.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 test/application/events.js diff --git a/test/application/events.js b/test/application/events.js new file mode 100644 index 000000000..0788b1911 --- /dev/null +++ b/test/application/events.js @@ -0,0 +1,32 @@ + +'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'].forEach(event => app.on(event, () => emitted.push(event))); + + app.use((ctx, next) => { + emitted.push('fistMiddleWare'); + ctx.body = 'hello!'; + return next(); + }); + + app.use(ctx => { + emitted.push('lastMiddleware'); + }); + + const server = app.listen(); + + await request(server) + .get('/') + .expect(200); + + assert.deepStrictEqual(emitted, ['request', 'fistMiddleWare', 'lastMiddleware', 'respond', 'responded']); + }); +}); From c719bdbd1d9ad50c6f8830510b757e6944d70a50 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 14:42:34 -0400 Subject: [PATCH 06/13] add custom event just in case --- test/application/events.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/application/events.js b/test/application/events.js index 0788b1911..4d642ffe7 100644 --- a/test/application/events.js +++ b/test/application/events.js @@ -9,7 +9,7 @@ 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'].forEach(event => app.on(event, () => emitted.push(event))); + ['request', 'respond', 'responded', 'error', 'customEvent'].forEach(event => app.on(event, () => emitted.push(event))); app.use((ctx, next) => { emitted.push('fistMiddleWare'); @@ -17,6 +17,11 @@ describe('app emits events', () => { return next(); }); + app.use((ctx, next) => { + ctx.app.emit('customEvent'); + return next(); + }); + app.use(ctx => { emitted.push('lastMiddleware'); }); @@ -27,6 +32,6 @@ describe('app emits events', () => { .get('/') .expect(200); - assert.deepStrictEqual(emitted, ['request', 'fistMiddleWare', 'lastMiddleware', 'respond', 'responded']); + assert.deepStrictEqual(emitted, ['request', 'fistMiddleWare', 'customEvent', 'lastMiddleware', 'respond', 'responded']); }); }); From 0fc13d9fed1a7025fe43de8b3c4ec23edcd573b2 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 14:52:07 -0400 Subject: [PATCH 07/13] ensure once events and ctx param --- test/application/events.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/application/events.js b/test/application/events.js index 4d642ffe7..eeddf9f7f 100644 --- a/test/application/events.js +++ b/test/application/events.js @@ -24,14 +24,24 @@ describe('app emits events', () => { app.use(ctx => { emitted.push('lastMiddleware'); + console.log('Called'); }); 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); }); }); From e4bf680bfa64951dbfbb10d724b6573d881f8e78 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 14:52:32 -0400 Subject: [PATCH 08/13] remove console.log --- test/application/events.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/application/events.js b/test/application/events.js index eeddf9f7f..14c72e77a 100644 --- a/test/application/events.js +++ b/test/application/events.js @@ -24,7 +24,6 @@ describe('app emits events', () => { app.use(ctx => { emitted.push('lastMiddleware'); - console.log('Called'); }); const server = app.listen(); From 2d181ddbb62e7b0f13cc51716ca67da8d7982a00 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 15:05:24 -0400 Subject: [PATCH 09/13] add error event test --- test/application/events.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/application/events.js b/test/application/events.js index 14c72e77a..1d60664c6 100644 --- a/test/application/events.js +++ b/test/application/events.js @@ -43,4 +43,31 @@ describe('app emits events', () => { 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 = []; + app.on('error', err => emitted.push(err)); + + 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.length, 1); + assert.ok(emitted[0] instanceof TypeError); + assert.strictEqual(onceEvents, 1); + }); }); From 6b996ba3aa39889defce71a398cd07de050aa513 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 21:27:47 -0400 Subject: [PATCH 10/13] clearInterval on `responded` in example --- docs/api/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api/index.md b/docs/api/index.md index 1957045c9..ee99a91d8 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -269,6 +269,7 @@ Note: clearInterval(idleInterval); }) .on('responded', () => { + clearInterval(idleInterval); idleInterval = setInterval(onIdle10000ms, IDLE_INTERVAL); }) ``` From 31911c4ef995637be6c316a3101fa810bf7dd73b Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 21:58:37 -0400 Subject: [PATCH 11/13] correct events in case of error --- lib/application.js | 12 +++++++++--- test/application/events.js | 5 ++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/application.js b/lib/application.js index db0e4977e..f7b7740df 100644 --- a/lib/application.js +++ b/lib/application.js @@ -156,12 +156,18 @@ module.exports = class Application extends Emitter { handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; + + const responding = Symbol('responding'); + this.once(responding, () => this.emit('respond', ctx)); + const onerror = err => { - if (null != err) ctx.onerror(err); - this.emit('responded', ctx); + if (null != err) { + ctx.onerror(err); + this.emit(responding); + } else this.emit('responded', ctx); }; const handleResponse = () => { - this.emit('respond', ctx); + this.emit(responding); respond(ctx); }; this.emit('request', ctx); diff --git a/test/application/events.js b/test/application/events.js index 1d60664c6..9bc1950ae 100644 --- a/test/application/events.js +++ b/test/application/events.js @@ -47,7 +47,7 @@ describe('app emits events', () => { it('should emit error event on middleware throw', async() => { const app = new Koa(); const emitted = []; - app.on('error', err => emitted.push(err)); + ['request', 'respond', 'responded', 'error'].forEach(event => app.on(event, () => emitted.push(event))); app.use((ctx, next) => { throw new TypeError('Hello Koa!'); @@ -66,8 +66,7 @@ describe('app emits events', () => { .get('/') .expect(500); - assert.deepStrictEqual(emitted.length, 1); - assert.ok(emitted[0] instanceof TypeError); + assert.deepStrictEqual(emitted, ['request', 'error', 'respond', 'responded']); assert.strictEqual(onceEvents, 1); }); }); From 4107093c3e8e2dfc83c33105d0f928078849eac0 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 22:15:24 -0400 Subject: [PATCH 12/13] use `server.timeout` in example --- docs/api/index.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/api/index.md b/docs/api/index.md index ee99a91d8..47f5c458b 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -261,16 +261,17 @@ Note: More advanced example, use events to detect that server is idling for some time: ```js - const onIdle10000ms = () => { console.warn('Server is idle for 10 seconds!'); } - const IDLE_INTERVAL = 10000; - let idleInterval = setInterval(onIdle10000ms, IDLE_INTERVAL); + 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(onIdle10000ms, IDLE_INTERVAL); + idleInterval = setInterval(onIdle, IDLE_INTERVAL); }) ``` From 4d70048c724bfaf65089547f5fe8b6d579fb96e9 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Thu, 17 Oct 2019 23:09:48 -0400 Subject: [PATCH 13/13] attach `responded` to the socket --- docs/api/index.md | 2 ++ lib/application.js | 3 ++- test/application/events.js | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/api/index.md b/docs/api/index.md index 47f5c458b..e2c019016 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -248,6 +248,8 @@ Note: }) ``` + 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: diff --git a/lib/application.js b/lib/application.js index f7b7740df..63e3bdf02 100644 --- a/lib/application.js +++ b/lib/application.js @@ -164,7 +164,7 @@ module.exports = class Application extends Emitter { if (null != err) { ctx.onerror(err); this.emit(responding); - } else this.emit('responded', ctx); + } }; const handleResponse = () => { this.emit(responding); @@ -172,6 +172,7 @@ module.exports = class Application extends Emitter { }; 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 index 9bc1950ae..ffa1fd69b 100644 --- a/test/application/events.js +++ b/test/application/events.js @@ -69,4 +69,24 @@ describe('app emits events', () => { 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']); + }); });