From 427d4bdd814289bb720dec34e68fe1cad6cd4058 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Fri, 7 Aug 2026 00:19:30 +1000 Subject: [PATCH] fix(proxy): skip empty passthrough body streams --- .../src/runtime/server/proxy-handler.ts | 4 ++-- test/unit/proxy-handler-body.test.ts | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/script/src/runtime/server/proxy-handler.ts b/packages/script/src/runtime/server/proxy-handler.ts index 6fa2ffa7..7beac922 100644 --- a/packages/script/src/runtime/server/proxy-handler.ts +++ b/packages/script/src/runtime/server/proxy-handler.ts @@ -520,7 +520,7 @@ export default defineEventHandler(async (event) => { // Resolve the fetch body: passthrough streams the raw request, otherwise serialize let fetchBody: BodyInit | undefined - if (passthroughBody) { + if (passthroughBody && originalHeaders['content-length'] !== '0') { fetchBody = getRequestWebStream(event) as BodyInit | undefined } else if (body !== undefined) { @@ -538,7 +538,7 @@ export default defineEventHandler(async (event) => { credentials: 'omit', // Don't send cookies to third parties signal: controller.signal, redirect: 'manual', - duplex: passthroughBody ? 'half' : undefined, + duplex: fetchBody instanceof ReadableStream ? 'half' : undefined, } response = await network.fetch(targetUrl, requestInit) clearTimeout(timeoutId) diff --git a/test/unit/proxy-handler-body.test.ts b/test/unit/proxy-handler-body.test.ts index a153a21f..47b71a5d 100644 --- a/test/unit/proxy-handler-body.test.ts +++ b/test/unit/proxy-handler-body.test.ts @@ -40,6 +40,8 @@ describe('proxy handler request bodies (#836)', () => { let capturedBody = Buffer.alloc(0) let capturedContentLength: string | undefined let capturedContentType: string | undefined + let capturedFetchBody: BodyInit | null | undefined + let capturedFetchDuplex: 'half' | undefined let capturedUrl = '' let releaseStream: (() => void) | undefined const realFetch = globalThis.fetch @@ -81,6 +83,8 @@ describe('proxy handler request bodies (#836)', () => { const requestUrl = input instanceof Request ? input.url : String(input) const url = new URL(requestUrl) if (url.hostname === 'upstream.test') { + capturedFetchBody = init?.body + capturedFetchDuplex = (init as RequestInit & { duplex?: 'half' } | undefined)?.duplex const redirected = `http://127.0.0.1:${upstreamPort}${url.pathname}${url.search}` return realFetch(redirected, init) } @@ -98,6 +102,8 @@ describe('proxy handler request bodies (#836)', () => { capturedBody = Buffer.alloc(0) capturedContentLength = undefined capturedContentType = undefined + capturedFetchBody = undefined + capturedFetchDuplex = undefined capturedUrl = '' releaseStream = undefined }) @@ -120,10 +126,28 @@ describe('proxy handler request bodies (#836)', () => { }) expect(response.status).toBe(200) + expect(capturedFetchBody).toBeInstanceOf(ReadableStream) + expect(capturedFetchDuplex).toBe('half') expect(capturedBody.equals(compressed)).toBe(true) expect(capturedContentType).toBe('text/plain') }) + it('forwards an explicitly empty opaque POST without a body stream (#853)', async () => { + const response = await realFetch(`http://127.0.0.1:${proxyPort}/_scripts/p/upstream.test/measurement/conversion`, { + method: 'POST', + headers: { + 'content-length': '0', + 'content-type': 'text/plain;charset=UTF-8', + }, + }) + + expect(response.status).toBe(200) + expect(capturedFetchBody).toBeUndefined() + expect(capturedFetchDuplex).toBeUndefined() + expect(capturedBody).toHaveLength(0) + expect(capturedContentLength).toBe('0') + }) + it('rejects an allowlisted local network target before the upstream fetch', async () => { const response = await realFetch(`http://127.0.0.1:${proxyPort}/_scripts/p/127.0.0.1/private`)