Skip to content

Commit ecae314

Browse files
committed
fix: don't timeout while waiting for client to send request (#1604)
* fix: don't timeout while waiting for client to send request When e.g. uploading a large file from the client one would always get a headers timeout even though everything was working and the file was being in progress of sending. * fixuP
1 parent fa9fd90 commit ecae314

6 files changed

Lines changed: 69 additions & 17 deletions

File tree

.github/workflows/nodejs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
exclude: |
2424
- runs-on: windows-latest
2525
node-version: 16
26+
- node-version: 12
2627
automerge:
2728
if: >
2829
github.event_name == 'pull_request' &&

docs/api/Client.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Returns: `Client`
1818
### Parameter: `ClientOptions`
1919

2020
* **bodyTimeout** `number | null` (optional) - Default: `30e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds.
21-
* **headersTimeout** `number | null` (optional) - Default: `30e3` - The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds.
21+
* **headersTimeout** `number | null` (optional) - Default: `30e3` - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds.
2222
* **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout` when overridden by *keep-alive* hints from the server. Defaults to 10 minutes.
2323
* **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds.
2424
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `1e3` - A number subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 1 second.

docs/api/Dispatcher.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
199199
* **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
200200
* **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
201201
* **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds.
202-
* **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds.
202+
* **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds.
203203
* **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server.
204204

205205
#### Parameter: `DispatchHandler`

lib/client.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -889,8 +889,10 @@ function onParserTimeout (parser) {
889889

890890
/* istanbul ignore else */
891891
if (timeoutType === TIMEOUT_HEADERS) {
892-
assert(!parser.paused, 'cannot be paused while waiting for headers')
893-
util.destroy(socket, new HeadersTimeoutError())
892+
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
893+
assert(!parser.paused, 'cannot be paused while waiting for headers')
894+
util.destroy(socket, new HeadersTimeoutError())
895+
}
894896
} else if (timeoutType === TIMEOUT_BODY) {
895897
if (!parser.paused) {
896898
util.destroy(socket, new BodyTimeoutError())
@@ -1641,7 +1643,18 @@ class AsyncWriter {
16411643
this.bytesWritten += len
16421644

16431645
const ret = socket.write(chunk)
1646+
16441647
request.onBodySent(chunk)
1648+
1649+
if (!ret) {
1650+
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
1651+
// istanbul ignore else: only for jest
1652+
if (socket[kParser].timeout.refresh) {
1653+
socket[kParser].timeout.refresh()
1654+
}
1655+
}
1656+
}
1657+
16451658
return ret
16461659
}
16471660

test/request-timeout.js

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,27 @@ const {
1818
test('request timeout', (t) => {
1919
t.plan(1)
2020

21-
const clock = FakeTimers.install()
22-
t.teardown(clock.uninstall.bind(clock))
23-
2421
const server = createServer((req, res) => {
2522
setTimeout(() => {
2623
res.end('hello')
27-
}, 100)
28-
clock.tick(100)
24+
}, 1000)
2925
})
3026
t.teardown(server.close.bind(server))
3127

3228
server.listen(0, () => {
33-
const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 })
29+
const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 500 })
3430
t.teardown(client.destroy.bind(client))
3531

3632
client.request({ path: '/', method: 'GET' }, (err, response) => {
3733
t.type(err, errors.HeadersTimeoutError)
3834
})
39-
40-
clock.tick(50)
4135
})
4236
})
4337

4438
test('request timeout with readable body', (t) => {
4539
t.plan(1)
4640

47-
const clock = FakeTimers.install()
48-
t.teardown(clock.uninstall.bind(clock))
49-
5041
const server = createServer((req, res) => {
51-
clock.tick(100)
5242
})
5343
t.teardown(server.close.bind(server))
5444

@@ -57,7 +47,7 @@ test('request timeout with readable body', (t) => {
5747
t.teardown(() => unlinkSync(tempfile))
5848

5949
server.listen(0, () => {
60-
const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 })
50+
const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 1e3 })
6151
t.teardown(client.destroy.bind(client))
6252

6353
const body = createReadStream(tempfile)

test/request-timeout2.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const { Client } = require('..')
5+
const { createServer } = require('http')
6+
const { Readable } = require('stream')
7+
8+
test('request timeout with slow readable body', (t) => {
9+
t.plan(1)
10+
11+
const server = createServer(async (req, res) => {
12+
let str = ''
13+
for await (const x of req) {
14+
str += x
15+
}
16+
res.end(str)
17+
})
18+
t.teardown(server.close.bind(server))
19+
20+
server.listen(0, () => {
21+
const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 })
22+
t.teardown(client.close.bind(client))
23+
24+
const body = new Readable({
25+
read () {
26+
if (this._reading) {
27+
return
28+
}
29+
this._reading = true
30+
31+
this.push('asd')
32+
setTimeout(() => {
33+
this.push('asd')
34+
this.push(null)
35+
}, 2e3)
36+
}
37+
})
38+
client.request({
39+
path: '/',
40+
method: 'POST',
41+
headersTimeout: 1e3,
42+
body
43+
}, async (err, response) => {
44+
t.error(err)
45+
await response.body.dump()
46+
})
47+
})
48+
})

0 commit comments

Comments
 (0)