nodejs / nodejs/undici

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

Open
#5,728 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.