Skip to content

fetch follow does not dump 3xx bodies and hangs when the redirect body is large #5728

Description

@cdloh

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions