From f436ea65ed0b9f6c29c6c55153f026e8458d1711 Mon Sep 17 00:00:00 2001 From: Luis Guideti Date: Wed, 28 Jan 2026 15:30:22 -0400 Subject: [PATCH 1/6] fix(response): handle zero Content-Length correctly for chunked and non-chunked responses Some HTTP servers incorrectly send `Content-Length: 0` together with `Transfer-Encoding: chunked`. According to RFC 7230, chunked transfer encoding overrides Content-Length and the body may still contain data. Previously, the client treated any response with Content-Length=0 as having an empty body, which caused valid chunked responses to be discarded. This change only treats Content-Length=0 as empty when the response is not chunked, preserving correct behavior for misconfigured servers. --- packages/openapi-fetch/src/index.js | 2 +- .../test/common/response.test.ts | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index be3b153a3..295142989 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -231,7 +231,7 @@ export default function createClient(clientOptions) { } // handle empty content - if (response.status === 204 || request.method === "HEAD" || response.headers.get("Content-Length") === "0") { + if (response.status === 204 || request.method === "HEAD" || (response.headers.get("Content-Length") === "0" && !response.headers.get("Transfer-Encoding")?.includes("chunked"))) { return response.ok ? { data: undefined, response } : { error: undefined, response }; } diff --git a/packages/openapi-fetch/test/common/response.test.ts b/packages/openapi-fetch/test/common/response.test.ts index ca7d21497..52b1ea7dd 100644 --- a/packages/openapi-fetch/test/common/response.test.ts +++ b/packages/openapi-fetch/test/common/response.test.ts @@ -192,4 +192,40 @@ describe("response", () => { } }); }); + + describe("chunked transfer with zero Content-Length", () => { + test("does not treat chunked body with Content-Length: 0 as empty", async () => { + const mock = [{ id: 1 }]; + const client = createObservedClient( + {}, + async () => + Response.json(mock, { + status: 200, + headers: { "Content-Length": "0", "Transfer-Encoding": "chunked" }, + }), + ); + + const { data, error, response } = await client.GET("/resources"); + expect(response.status).toBe(200); + expect(error).toBeUndefined(); + expect(data).toEqual(mock); + }); + }); + describe("Content-Length: 0 without chunked", () => { + test("treats as empty when not chunked", async () => { + const client = createObservedClient( + {}, + async () => + Response.json([{ id: 1 }], { + status: 200, + headers: { "Content-Length": "0" }, + }), + ); + + const { data, error, response } = await client.GET("/resources"); + expect(response.status).toBe(200); + expect(error).toBeUndefined(); + expect(data).toBeUndefined(); + }); + }); }); From 10bf2d88341d76f0a89949bed42fa6b1b280da58 Mon Sep 17 00:00:00 2001 From: Luis Guideti Date: Thu, 29 Jan 2026 10:04:11 -0400 Subject: [PATCH 2/6] chore: fix linting errors --- packages/openapi-fetch/src/index.js | 15 ++++++++---- .../test/common/response.test.ts | 24 ++++++++----------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 295142989..d8fbc0e58 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -89,8 +89,8 @@ export default function createClient(clientOptions) { const finalHeaders = mergeHeaders( // with no body, we should not to set Content-Type serializedBody === undefined || - // if serialized body is FormData; browser will correctly set Content-Type & boundary expression - serializedBody instanceof FormData + // if serialized body is FormData; browser will correctly set Content-Type & boundary expression + serializedBody instanceof FormData ? {} : { "Content-Type": "application/json", @@ -231,7 +231,12 @@ export default function createClient(clientOptions) { } // handle empty content - if (response.status === 204 || request.method === "HEAD" || (response.headers.get("Content-Length") === "0" && !response.headers.get("Transfer-Encoding")?.includes("chunked"))) { + if ( + response.status === 204 || + request.method === "HEAD" || + (response.headers.get("Content-Length") === "0" && + !response.headers.get("Transfer-Encoding")?.includes("chunked")) + ) { return response.ok ? { data: undefined, response } : { error: undefined, response }; } @@ -603,8 +608,8 @@ export function defaultBodySerializer(body, headers) { if (headers) { const contentType = headers.get instanceof Function - ? (headers.get("Content-Type") ?? headers.get("content-type")) - : (headers["Content-Type"] ?? headers["content-type"]); + ? headers.get("Content-Type") ?? headers.get("content-type") + : headers["Content-Type"] ?? headers["content-type"]; if (contentType === "application/x-www-form-urlencoded") { return new URLSearchParams(body).toString(); } diff --git a/packages/openapi-fetch/test/common/response.test.ts b/packages/openapi-fetch/test/common/response.test.ts index 52b1ea7dd..b97aae018 100644 --- a/packages/openapi-fetch/test/common/response.test.ts +++ b/packages/openapi-fetch/test/common/response.test.ts @@ -196,13 +196,11 @@ describe("response", () => { describe("chunked transfer with zero Content-Length", () => { test("does not treat chunked body with Content-Length: 0 as empty", async () => { const mock = [{ id: 1 }]; - const client = createObservedClient( - {}, - async () => - Response.json(mock, { - status: 200, - headers: { "Content-Length": "0", "Transfer-Encoding": "chunked" }, - }), + const client = createObservedClient({}, async () => + Response.json(mock, { + status: 200, + headers: { "Content-Length": "0", "Transfer-Encoding": "chunked" }, + }), ); const { data, error, response } = await client.GET("/resources"); @@ -213,13 +211,11 @@ describe("response", () => { }); describe("Content-Length: 0 without chunked", () => { test("treats as empty when not chunked", async () => { - const client = createObservedClient( - {}, - async () => - Response.json([{ id: 1 }], { - status: 200, - headers: { "Content-Length": "0" }, - }), + const client = createObservedClient({}, async () => + Response.json([{ id: 1 }], { + status: 200, + headers: { "Content-Length": "0" }, + }), ); const { data, error, response } = await client.GET("/resources"); From 57a7f9b30b5d40483cddc48c752d43135ca99a5c Mon Sep 17 00:00:00 2001 From: Luis Guideti Date: Thu, 29 Jan 2026 10:08:55 -0400 Subject: [PATCH 3/6] chore: revert accidental line changes --- packages/openapi-fetch/src/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index d8fbc0e58..2db07861f 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -89,8 +89,8 @@ export default function createClient(clientOptions) { const finalHeaders = mergeHeaders( // with no body, we should not to set Content-Type serializedBody === undefined || - // if serialized body is FormData; browser will correctly set Content-Type & boundary expression - serializedBody instanceof FormData + // if serialized body is FormData; browser will correctly set Content-Type & boundary expression + serializedBody instanceof FormData ? {} : { "Content-Type": "application/json", @@ -608,8 +608,8 @@ export function defaultBodySerializer(body, headers) { if (headers) { const contentType = headers.get instanceof Function - ? headers.get("Content-Type") ?? headers.get("content-type") - : headers["Content-Type"] ?? headers["content-type"]; + ? (headers.get("Content-Type") ?? headers.get("content-type")) + : (headers["Content-Type"] ?? headers["content-type"]); if (contentType === "application/x-www-form-urlencoded") { return new URLSearchParams(body).toString(); } From ef30a94165743b092db8dd83e15a9ed2a0299c6f Mon Sep 17 00:00:00 2001 From: Luis Guideti Date: Mon, 9 Feb 2026 11:47:15 -0400 Subject: [PATCH 4/6] chore: added changeset --- .changeset/empty-ghosts-sip.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/empty-ghosts-sip.md diff --git a/.changeset/empty-ghosts-sip.md b/.changeset/empty-ghosts-sip.md new file mode 100644 index 000000000..3073fc3ac --- /dev/null +++ b/.changeset/empty-ghosts-sip.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": minor +--- + +Do not treat Content-Length=0 as empty when Transfer-Encoding is chunked From a520958397521813ae14dfb518142de84c349610 Mon Sep 17 00:00:00 2001 From: Luis Guideti Date: Mon, 9 Feb 2026 12:09:50 -0400 Subject: [PATCH 5/6] Resolve merge conflicts --- packages/openapi-fetch/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 2db07861f..d44dba5f2 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -230,12 +230,12 @@ export default function createClient(clientOptions) { } } + const contentLength = response.headers.get("Content-Length"); // handle empty content if ( response.status === 204 || request.method === "HEAD" || - (response.headers.get("Content-Length") === "0" && - !response.headers.get("Transfer-Encoding")?.includes("chunked")) + (contentLength && !response.headers.get("Transfer-Encoding")?.includes("chunked")) ) { return response.ok ? { data: undefined, response } : { error: undefined, response }; } From 040c0395b9acb72d43b3f66ebd96011b2427cccf Mon Sep 17 00:00:00 2001 From: Luis Guideti Date: Mon, 9 Feb 2026 12:14:06 -0400 Subject: [PATCH 6/6] Properly check if content length is zero --- packages/openapi-fetch/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index d44dba5f2..74044ad41 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -235,7 +235,7 @@ export default function createClient(clientOptions) { if ( response.status === 204 || request.method === "HEAD" || - (contentLength && !response.headers.get("Transfer-Encoding")?.includes("chunked")) + (contentLength === "0" && !response.headers.get("Transfer-Encoding")?.includes("chunked")) ) { return response.ok ? { data: undefined, response } : { error: undefined, response }; }