Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2266,6 +2266,15 @@ async function httpNetworkFetch (
const willFollow = location && request.redirect === 'follow' &&
redirectStatusSet.has(status)

// When this 3xx response will be followed, its body is discarded and
// never exposed to the caller. Drain it (ignore the bytes but keep
// reading, see onResponseData) instead of buffering it into this.body,
// otherwise a redirect body larger than the stream's highWaterMark
// applies backpressure that pauses and pins the connection, hanging
// the follow-up request on a connection-limited dispatcher.
// See https://github.com/nodejs/undici/issues/5728
this.willFollow = willFollow

const decoders = []

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
Expand Down Expand Up @@ -2335,7 +2344,10 @@ async function httpNetworkFetch (
},

onResponseData (controller, chunk) {
if (fetchParams.controller.dump) {
// Discard the body of a 3xx response that is about to be followed,
// but keep reading (do not pause) so the connection drains and is
// released back to the pool. https://github.com/nodejs/undici/issues/5728
if (fetchParams.controller.dump || this.willFollow) {
return
}

Expand Down
41 changes: 41 additions & 0 deletions test/issue-5728.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict'

// Repro for https://github.com/nodejs/undici/issues/5728
// fetch({ redirect: 'follow' }) does not drain the 3xx redirect response body.
// When the redirect body is larger than the stream highWaterMark the socket
// stays `running`, so with an Agent limited to a single connection the
// follow-up request can never acquire the pinned socket and fetch hangs.
//
// Run: node --test test/issue-5728.js

const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { fetch, Agent } = require('..')

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)
// Larger than the default 64 KiB highWaterMark so the body is not fully
// buffered/drained implicitly.
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)
})
Loading