diff --git a/README.md b/README.md index fe5c2d5d966..214401322a7 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,8 @@ Options: Default: `null`. - `idleTimeout`, the timeout after which a socket with no active requests - will be released and no longer re-used for subsequent requests. + will be released and no longer re-used for subsequent requests. This value + is an upper bound and might be reduced by keep-alive hints from the server. Default: `30e3` milliseconds (30s). - `requestTimeout`, the timeout after which a request will time out, in diff --git a/lib/client.js b/lib/client.js index 27e72dd38a4..1796c65488a 100644 --- a/lib/client.js +++ b/lib/client.js @@ -46,6 +46,7 @@ const { kSocket, kSocketPath, kEnqueue, + kKeepAliveTimeout, kMaxHeadersSize, kHeadersTimeout } = require('./symbols') @@ -133,6 +134,7 @@ class Client extends EventEmitter { this[kSocketPath] = socketPath this[kSocketTimeout] = socketTimeout == null ? 30e3 : socketTimeout this[kIdleTimeout] = idleTimeout == null ? 30e3 : idleTimeout + this[kKeepAliveTimeout] = this[kIdleTimeout] this[kRequestTimeout] = requestTimeout == null ? 30e3 : requestTimeout this[kClosed] = false this[kDestroyed] = false @@ -415,6 +417,8 @@ class Parser extends HTTPParser { [HTTPParser.kOnHeadersComplete] (versionMajor, versionMinor, rawHeaders, method, url, statusCode, statusMessage, upgrade, shouldKeepAlive) { + this.headers = util.parseHeaders(rawHeaders, this.headers) + const { client, socket, resumeSocket, headers } = this const request = client[kQueue][client[kRunningIdx]] @@ -435,12 +439,22 @@ class Parser extends HTTPParser { return true } + if (headers['keep-alive']) { + const m = headers['keep-alive'].match(/timeout=(\d+)/) + if (m) { + // Set timeout to half of hint to account for timing inaccuracies. + client[kKeepAliveTimeout] = Math.min(parseInt(m[1]) * 1000 / 2, client[kIdleTimeout]) + } + } else { + client[kKeepAliveTimeout] = client[kIdleTimeout] + } + // TODO: More statusCode validation? if (statusCode < 200) { // request.onInfo(statusCode, util.parseHeaders(rawHeaders, headers)) } else { - request.onHeaders(statusCode, util.parseHeaders(rawHeaders, headers), resumeSocket) + request.onHeaders(statusCode, headers, resumeSocket) } return request.method === 'HEAD' || statusCode < 200 @@ -663,6 +677,14 @@ function resume (client) { return } + if ( + client.running === 0 && + client[kSocket] && + client[kSocket].timeout !== client[kKeepAliveTimeout] + ) { + client[kSocket].setTimeout(client[kKeepAliveTimeout]) + } + if (client.size === 0) { if (client[kClosed]) { client.destroy(util.nop) @@ -675,14 +697,6 @@ function resume (client) { return } - if ( - client.running === 0 && - client[kSocket] && - client[kSocket].timeout !== client[kIdleTimeout] - ) { - client[kSocket].setTimeout(client[kIdleTimeout]) - } - if (client[kRunningIdx] > 256) { client[kQueue].splice(0, client[kRunningIdx]) client[kPendingIdx] -= client[kRunningIdx] diff --git a/lib/symbols.js b/lib/symbols.js index fff7e96e1de..52bd8111ff3 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -7,6 +7,7 @@ module.exports = { kSocketTimeout: Symbol('socket timeout'), kIdleTimeout: Symbol('idle timeout'), kRequestTimeout: Symbol('request timeout'), + kKeepAliveTimeout: Symbol('keep alive timeout'), kServerName: Symbol('server name'), kTLSOpts: Symbol('TLS Options'), kClosed: Symbol('closed'), diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js new file mode 100644 index 00000000000..3edc5af78c1 --- /dev/null +++ b/test/client-keep-alive.js @@ -0,0 +1,40 @@ +'use strict' + +const { test } = require('tap') +const { Client } = require('..') +const { createServer } = require('net') + +test('keep-alive header', (t) => { + t.plan(2) + + const server = createServer((socket) => { + socket.write('HTTP/1.1 200 OK\r\n') + socket.write('Content-Length: 0\r\n') + socket.write('Keep-Alive: timeout=1s\r\n') + socket.write('Connection: keep-alive\r\n') + socket.write('\r\n\r\n') + }) + t.teardown(server.close.bind(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.teardown(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'GET', + idleTimeout: 30e3 + }, (err, { body }) => { + t.error(err) + body.on('end', () => { + const timeout = setTimeout(() => { + t.fail() + }, 1e3) + client.on('disconnect', () => { + t.pass() + clearTimeout(timeout) + }) + }).resume() + }) + }) +})