Repository navigation
Should Fetch retry reusable request bodies after HTTP/2 GOAWAY? #5912
Description
This would solve...
When an HTTP/2 server sends GOAWAY, it can say that a request stream was not processed. Undici can retry some requests in this case. With Fetch, a string body becomes a one-use stream. The retry code cannot send it again, so the request times out.
The example sends foo=bar on stream 1. GOAWAY says that stream 1 was not processed. On current main, Fetch times out and does not open a second stream.
Reproduction
Run this from the Undici repo root after installing the development dependencies:
const { constants, createSecureServer } = require('node:http2')
const { once } = require('node:events')
const pem = require('@metcoder95/https-pem')
const { Client, fetch } = require('./')
async function main () {
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
let streams = 0
let received = ''
server.on('session', session => session.on('error', () => {}))
server.on('stream', (stream) => {
stream.on('error', () => {})
if (++streams === 1) {
stream.session.goaway(constants.NGHTTP2_NO_ERROR, 0)
stream.resume()
return
}
stream.setEncoding('utf8')
stream.on('data', chunk => { received += chunk })
stream.on('end', () => {
stream.respond({ ':status': 200 })
stream.end('ok')
})
})
server.listen(0)
await once(server, 'listening')
const url = `https://localhost:${server.address().port}/`
const client = new Client(url, {
allowH2: true,
connect: { rejectUnauthorized: false }
})
try {
const response = await fetch(url, {
method: 'POST',
body: 'foo=bar',
dispatcher: client,
signal: AbortSignal.timeout(5000)
})
console.log(response.status, { streams, received })
} catch (error) {
console.log(error.name, { streams, received })
} finally {
await client.destroy()
await new Promise(resolve => server.close(resolve))
}
}
main()On current main, this prints TimeoutError with streams: 1 and an empty received value. I expect it to print status 200 with streams: 2 and received: 'foo=bar'.
The implementation should look like...
Fetch could create a new stream for bodies that always produce the same bytes, such as strings, byte arrays, and Blobs. ReadableStream and async iterable bodies should stay one-use.
I have also considered...
Recreating FormData may create a new multipart boundary. The body could then disagree with the existing Content-Type. Should FormData stay non-replayable unless Undici keeps the original body and boundary? Should this also work through RetryAgent?
Additional context
This came from PR #5909, which was closed with a request to open an issue first. Issue #3433 is about a later request after GOAWAY. This issue is about retrying the request GOAWAY says was not processed.
Activity
KhafraDev commentedon Sep 27, 2026
https://github.com/KhafraDev/undici/tree/fetch/issue-5912