From c9bf8b7b9d7723805cc75e4e41e01134c7577d24 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Wed, 17 Dec 2025 20:25:24 -0300 Subject: [PATCH] fix: update Content-Type header when body is re-extracted on redirect When a multipart/form-data POST request encounters a redirect (e.g., 307), the body is re-extracted via safelyExtractBody(), which generates a new boundary. Previously, only the body was captured and the new Content-Type was discarded, leaving the header with the stale boundary. This fix captures the Content-Type returned by safelyExtractBody() and updates the request headers accordingly, ensuring the boundary in the header matches the boundary in the body. Fixes: https://github.com/nodejs/undici/issues/4065 --- lib/web/fetch/index.js | 6 +++++- test/fetch/redirect.js | 46 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index 0f0a94d37fe..4db36b3f372 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -1322,7 +1322,11 @@ function httpRedirectFetch (fetchParams, response) { // value of safely extracting request’s body’s source. if (request.body != null) { assert(request.body.source != null) - request.body = safelyExtractBody(request.body.source)[0] + const [body, contentType] = safelyExtractBody(request.body.source) + request.body = body + if (contentType) { + request.headersList.set('content-type', contentType, true) + } } // 15. Let timingInfo be fetchParams’s timing info. diff --git a/test/fetch/redirect.js b/test/fetch/redirect.js index 913ec7972e9..291a9a1d19b 100644 --- a/test/fetch/redirect.js +++ b/test/fetch/redirect.js @@ -3,7 +3,7 @@ const { test } = require('node:test') const { createServer } = require('node:http') const { once } = require('node:events') -const { fetch } = require('../..') +const { fetch, FormData } = require('../..') const { closeServerAsPromise } = require('../utils/node-http') // https://github.com/nodejs/undici/issues/1776 @@ -75,3 +75,47 @@ test('Redirecting with a body does not fail to write body - #2543', async (t) => t.assert.strictEqual(await resp.text(), 'ok') t.assert.ok(resp.redirected) }) + +// https://github.com/nodejs/undici/issues/4065 +test('Redirecting with FormData updates Content-Type header boundary - #4065', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + if (req.url === '/redirect') { + res.writeHead(307, { location: '/target' }) + res.end() + return + } + + // Collect the request body and verify Content-Type boundary matches body boundary + const contentType = req.headers['content-type'] + const boundaryMatch = contentType?.match(/boundary=(.+)$/) + const headerBoundary = boundaryMatch?.[1] + + let body = '' + req.on('data', (chunk) => { body += chunk.toString() }) + req.on('end', () => { + // Extract boundary from the body (first line is --boundary) + const bodyBoundaryMatch = body.match(/^--(.+)\r\n/) + const bodyBoundary = bodyBoundaryMatch?.[1] + + // The header boundary must match the body boundary + t.assert.ok(headerBoundary, 'Content-Type header should have boundary') + t.assert.ok(bodyBoundary, 'Body should have boundary') + t.assert.strictEqual(headerBoundary, bodyBoundary, 'Content-Type boundary must match body boundary') + + res.end('ok') + }) + }).listen(0) + + t.after(closeServerAsPromise(server)) + await once(server, 'listening') + + const formData = new FormData() + formData.append('field', 'value') + + const resp = await fetch(`http://localhost:${server.address().port}/redirect`, { + method: 'POST', + body: formData + }) + t.assert.strictEqual(await resp.text(), 'ok') + t.assert.ok(resp.redirected) +})