From 62f62816608823e1b152b27d3209092adc77b454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darwin=20=E2=9D=A4=EF=B8=8F=E2=9D=A4=EF=B8=8F=E2=9D=A4?= =?UTF-8?q?=EF=B8=8F?= <71545960+darwin808@users.noreply.github.com> Date: Thu, 9 Apr 2026 03:07:51 +0800 Subject: [PATCH] fix(fetch): remove Content-Type without boundary for FormData (#7314) * fix(fetch): remove Content-Type without boundary for FormData When using the fetch adapter with FormData and manually setting Content-Type to 'multipart/form-data' (without boundary), the request would fail because the boundary is required. This fix detects when Content-Type is set to multipart/form-data without a boundary and removes it, allowing fetch to automatically set the correct Content-Type header with the proper boundary. Fixes #7054 * chore: update fixes --------- Co-authored-by: Jason Saayman --- lib/adapters/fetch.js | 13 +++++++++ tests/unit/adapters/fetch.test.js | 48 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/lib/adapters/fetch.js b/lib/adapters/fetch.js index 6588ddf0..2b2d8c62 100644 --- a/lib/adapters/fetch.js +++ b/lib/adapters/fetch.js @@ -221,6 +221,19 @@ const factory = (env) => { // see https://github.com/cloudflare/workerd/issues/902 const isCredentialsSupported = isRequestSupported && 'credentials' in Request.prototype; + // If data is FormData and Content-Type is multipart/form-data without boundary, + // delete it so fetch can set it correctly with the boundary + if (utils.isFormData(data)) { + const contentType = headers.getContentType(); + if ( + contentType && + /^multipart\/form-data/i.test(contentType) && + !/boundary=/i.test(contentType) + ) { + headers.delete('content-type'); + } + } + const resolvedOptions = { ...fetchOptions, signal: composedSignal, diff --git a/tests/unit/adapters/fetch.test.js b/tests/unit/adapters/fetch.test.js index 43f21013..0be0ab57 100644 --- a/tests/unit/adapters/fetch.test.js +++ b/tests/unit/adapters/fetch.test.js @@ -550,6 +550,54 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => await stopHTTPServer(server); } }); + + it('should remove manually set Content-Type without boundary for FormData', async () => { + const form = new FormData(); + form.append('foo', 'bar'); + + const server = await startHTTPServer( + (req, res) => { + const contentType = req.headers['content-type']; + assert.match(contentType, /^multipart\/form-data; boundary=/i); + res.end('OK'); + }, + { port: SERVER_PORT } + ); + + try { + await fetchAxios.post(`http://localhost:${server.address().port}/form`, form, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + } finally { + await stopHTTPServer(server); + } + }); + + it('should preserve Content-Type if it already has boundary', async () => { + const form = new FormData(); + form.append('foo', 'bar'); + + const customBoundary = '----CustomBoundary123'; + + const server = await startHTTPServer( + (req, res) => { + const contentType = req.headers['content-type']; + assert.ok(contentType.includes(customBoundary)); + res.end('OK'); + }, + { port: SERVER_PORT } + ); + + try { + await fetchAxios.post(`http://localhost:${server.address().port}/form`, form, { + headers: { + 'Content-Type': `multipart/form-data; boundary=${customBoundary}`, + }, + }); + } finally { + await stopHTTPServer(server); + } + }); }); describe('env config', () => {