Skip to content

Commit 9d41b62

Browse files
committed
fix: throw on retry when payload is consumed
1 parent 4b0921c commit 9d41b62

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

lib/handler/retry-handler.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class RetryHandler {
6868
this.end = null
6969
this.etag = null
7070
this.resume = null
71+
this.consumed = false
7172

7273
// Handle possible onConnect duplication
7374
this.handler.onConnect(reason => {
@@ -193,6 +194,19 @@ class RetryHandler {
193194
this.resume = null
194195

195196
if (statusCode !== 206) {
197+
// Abort when status code is not partial content
198+
// and payload is already consumed because downstream
199+
// will concatenate response from two request wrongly.
200+
if (this.consumed) {
201+
this.abort(
202+
new RequestRetryError('unsupported retry when payload is consumed', statusCode, {
203+
headers,
204+
data: { count: this.retryCount }
205+
})
206+
)
207+
return false
208+
}
209+
196210
return true
197211
}
198212

@@ -294,6 +308,7 @@ class RetryHandler {
294308
}
295309

296310
onData (chunk) {
311+
this.consumed = true
297312
this.start += chunk.length
298313

299314
return this.handler.onData(chunk)

test/issue-3356.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict'
2+
3+
const { tspl } = require('@matteo.collina/tspl')
4+
const { test, after } = require('node:test')
5+
const { createServer } = require('node:http')
6+
const { once } = require('node:events')
7+
8+
const { fetch, Agent, RetryAgent } = require('..')
9+
10+
test('https://github.com/nodejs/undici/issues/3356', async (t) => {
11+
t = tspl(t, { plan: 3 })
12+
13+
let shouldRetry = true
14+
const server = createServer()
15+
server.on('request', (req, res) => {
16+
res.writeHead(200, { 'content-type': 'text/plain' })
17+
if (shouldRetry) {
18+
shouldRetry = false
19+
20+
res.flushHeaders()
21+
res.write('h')
22+
setTimeout(() => { res.end('ello world!') }, 100)
23+
} else {
24+
res.end('hello world!')
25+
}
26+
})
27+
28+
server.listen(0)
29+
30+
await once(server, 'listening')
31+
32+
after(async () => {
33+
server.close()
34+
35+
await once(server, 'close')
36+
})
37+
38+
const agent = new RetryAgent(new Agent({ bodyTimeout: 50 }), {
39+
errorCodes: ['UND_ERR_BODY_TIMEOUT']
40+
})
41+
42+
const response = await fetch(`http://localhost:${server.address().port}`, {
43+
dispatcher: agent
44+
})
45+
setTimeout(async () => {
46+
try {
47+
t.equal(response.status, 200)
48+
// consume response
49+
await response.text()
50+
} catch (err) {
51+
t.equal(err.name, 'TypeError')
52+
t.equal(err.cause.code, 'UND_ERR_REQ_RETRY')
53+
}
54+
}, 200)
55+
56+
await t.completed
57+
})

0 commit comments

Comments
 (0)