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
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 staysrunning. 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
Agentused asdispatcherand aconnectionslimit as each redirect can pin a pool slot and exhaust the pool.Reproduction
Standalone reproduction script:
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