diff --git a/config/config.default.js b/config/config.default.js index 90a013bbff..cc4fa18e29 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -299,6 +299,16 @@ module.exports = appInfo => { */ config.workerStartTimeout = 10 * 60 * 1000; + /** + * server timeout in milliseconds, default to 2 minutes. + * + * for special request, just use `ctx.req.setTimeout(ms)` + * + * @member {Number} Config#serverTimeout + * @see https://nodejs.org/api/http.html#http_server_timeout + */ + config.serverTimeout = null; + /** * * @member {Object} Config#cluster diff --git a/lib/application.js b/lib/application.js index a17e3e21ed..bcf085fa72 100644 --- a/lib/application.js +++ b/lib/application.js @@ -3,6 +3,7 @@ const path = require('path'); const fs = require('fs'); const ms = require('ms'); +const is = require('is-type-of'); const graceful = require('graceful'); const http = require('http'); const cluster = require('cluster-client'); @@ -154,6 +155,14 @@ class Application extends EggApplication { } } + _onServerTimeout(socket) { + const req = socket.parser.incoming; + if (req) { + this.coreLogger.warn('[http_server] A request `%s %s` timeout with client (%s:%d)', req.method, req.url, socket.remoteAddress, socket.remotePort); + } + socket.destroy(); + } + onServer(server) { // expose app.server this.server = server; @@ -170,6 +179,10 @@ class Application extends EggApplication { }); server.on('clientError', (err, socket) => this.onClientError(err, socket)); + + // server timeout + if (is.number(this.config.serverTimeout)) server.setTimeout(this.config.serverTimeout); + server.on('timeout', socket => this._onServerTimeout(socket)); } /** diff --git a/test/fixtures/apps/app-server-timeout/app/router.js b/test/fixtures/apps/app-server-timeout/app/router.js new file mode 100644 index 0000000000..aeb896a516 --- /dev/null +++ b/test/fixtures/apps/app-server-timeout/app/router.js @@ -0,0 +1,14 @@ +'use strict'; + +const { sleep } = require('mz-modules'); + +module.exports = app => { + app.get('/', async ctx => { + ctx.body = 'ok'; + }); + + app.get('/timeout', async ctx => { + await sleep(500); + ctx.body = 'timeout'; + }); +}; diff --git a/test/fixtures/apps/app-server-timeout/config/config.default.js b/test/fixtures/apps/app-server-timeout/config/config.default.js new file mode 100644 index 0000000000..4a74ce2a7f --- /dev/null +++ b/test/fixtures/apps/app-server-timeout/config/config.default.js @@ -0,0 +1,3 @@ +exports.keys = 'my keys'; + +exports.serverTimeout = 100; diff --git a/test/fixtures/apps/app-server-timeout/package.json b/test/fixtures/apps/app-server-timeout/package.json new file mode 100644 index 0000000000..9052c596dd --- /dev/null +++ b/test/fixtures/apps/app-server-timeout/package.json @@ -0,0 +1,3 @@ +{ + "name": "app-server-timeout" +} diff --git a/test/lib/cluster/app_worker.test.js b/test/lib/cluster/app_worker.test.js index 53cd30dac4..75da0c190c 100644 --- a/test/lib/cluster/app_worker.test.js +++ b/test/lib/cluster/app_worker.test.js @@ -3,7 +3,7 @@ const net = require('net'); const request = require('supertest'); const address = require('address'); -const assert = require('assert'); +const assert = require('assert-extends'); const sleep = require('mz-modules/sleep'); const utils = require('../../utils'); @@ -64,6 +64,29 @@ describe('test/lib/cluster/app_worker.test.js', () => { ]); }); + describe('server timeout', () => { + let app; + beforeEach(() => { + app = utils.cluster('apps/app-server-timeout'); + // app.debug(); + return app.ready(); + }); + afterEach(() => app.close()); + + it('should not timeout', () => { + return app.httpRequest() + .get('/') + .expect(200); + }); + + it('should timeout', async () => { + await assert.asyncThrows(() => { + return app.httpRequest().get('/timeout'); + }, /socket hang up/); + app.expect('stdout', /\[http_server] A request `GET \/timeout` timeout with client/); + }); + }); + describe('customized client error', () => { let app; beforeEach(() => {