fetch follow does not dump 3xx bodies and hangs when the redirect body is large
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 7.7k
- Forks
- 880
- Avg merge
- 2d 16h
- Merged PRs (30d)
- 68
Description
Bug Description
fetch({ redirect: 'follow' }) does not dump 3xx response bodies. If the redirect body is big enough (I think it just needs to be is larger than the unread stream highWaterMark) the connection stays running. The follow-up request then cannot use that connection.
request() appears to already ignore 3xx bodies and keeps reading. I'm unsure then if this is expected behaviour or a bug but thought it would be worth reporting regardless.
Additionally with redirect follow the intermediate 3xx responses are never exposed so it's impossible to clean them up / free the connection.
This is especially painful with an Agent used as dispatcher and a connections limit as each redirect can pin a pool slot and exhaust the pool.
Reproduction
Standalone reproduction script:
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { fetch, Agent } = require('undici') // use require('..') inside this repo
test('fetch follow does not pin the keep-alive socket on a 301 with a large body', { timeout: 15_000 }, async (t) => {
t.plan(3)
const redirectBody = Buffer.alloc(128 * 1024, 0x78)
const server = createServer((req, res) => {
if (req.url === '/redirect') {
res.writeHead(301, { Location: '/final' })
res.end(redirectBody)
return
}
res.end('ok')
})
const dispatcher = new Agent({ connections: 1, keepAliveTimeout: 10_000 })
t.after(async () => {
await dispatcher.destroy()
server.close()
})
server.listen(0)
await once(server, 'listening')
const url = `http://127.0.0.1:${server.address().port}/redirect`
const first = await fetch(url, { dispatcher, redirect: 'follow' })
t.assert.strictEqual(first.status, 200)
t.assert.strictEqual(await first.text(), 'ok')
t.assert.ok(first.redirected)
})
Expected Behavior
fetch follows the 301, dumps the unused redirect body, returns the /final 200, and releases the connection.
Actual Behavior
fetch never resolves. The only pooled socket is stuck on the unread 301 body, so the follow-up GET to /final queues forever.
Logs & Screenshots
✖ fetch follow does not pin the keep-alive socket on a 301 with a large body (15006.989667ms)
ℹ tests 1
ℹ pass 0
ℹ fail 0
ℹ cancelled 1
ℹ duration_ms 15208.42675
✖ failing tests:
test at test/repro-fetch-follow-redirect-body.js:11:1
✖ fetch follow does not pin the keep-alive socket on a 301 with a large body (15006.989667ms)
'test timed out after 15000ms'
Environment
OS: macOS 26.6 (Darwin 25.6.0 arm64)
Node.js version: v24.15.0
undici version: 7.25.0 (happens on main too)
Additional Context
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with test/repro-fetch-follow-redirect-body.js and reproduce the hang using fetch with redirect: 'follow' and an Agent limited to one connection. Compare the fetch redirect path with request(), which the issue says already keeps reading 3xx bodies. Done means the 301 body is consumed, the /final request completes, and the reproduction test passes without pinning the connection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nodejs
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100