Skip to content

Commit

Permalink
add support for keep-alive timeout server hint (#275)
Browse files Browse the repository at this point in the history
  • Loading branch information
ronag committed Jul 30, 2020
1 parent e0bcd30 commit 27754e9
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 6 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 20 additions & 5 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const {
kSocket,
kSocketPath,
kEnqueue,
kKeepAliveTimeout,
kMaxHeadersSize,
kHeadersTimeout
} = require('./symbols')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]]

Expand All @@ -435,12 +439,23 @@ class Parser extends HTTPParser {
return true
}

if (headers['keep-alive']) {
const m = headers['keep-alive'].match(/timeout=(\d+)/)
if (m) {
const keepAliveTimeout = Number(m[1]) * 1000
// Set timeout to half of hint to account for timing inaccuracies.
client[kKeepAliveTimeout] = Math.min(keepAliveTimeout - 500, client[kIdleTimeout])
}
} else {
client[kKeepAliveTimeout] = client[kIdleTimeout]
}

// TODO: More statusCode validation?

if (statusCode < 200) {
// request.onInfo(statusCode, util.parseHeaders(rawHeaders, headers))
if (statusCode >= 200) {
request.onHeaders(statusCode, headers, resumeSocket)
} else {
request.onHeaders(statusCode, util.parseHeaders(rawHeaders, headers), resumeSocket)
// TODO: Info
}

return request.method === 'HEAD' || statusCode < 200
Expand Down Expand Up @@ -670,9 +685,9 @@ function resume (client) {

if (
client[kSocket] &&
client[kSocket].timeout !== client[kIdleTimeout]
client[kSocket].timeout !== client[kKeepAliveTimeout]
) {
client[kSocket].setTimeout(client[kIdleTimeout])
client[kSocket].setTimeout(client[kKeepAliveTimeout])
}

if (client[kRunningIdx] > 0) {
Expand Down
1 change: 1 addition & 0 deletions lib/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
40 changes: 40 additions & 0 deletions test/client-keep-alive.js
Original file line number Diff line number Diff line change
@@ -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()
})
})
})

0 comments on commit 27754e9

Please sign in to comment.