From a38257ab46e6898b77a6600ffc1508b29f87dc2a Mon Sep 17 00:00:00 2001 From: jbarba Date: Mon, 20 Oct 2025 01:03:46 +0100 Subject: [PATCH] fix(openapi-fetch): avoid errors parsing empty JSON bodies (200 with no content) --- .changeset/wet-waves-turn.md | 5 +++ packages/openapi-fetch/src/index.js | 23 +++++++--- .../openapi-fetch/test/common/request.test.ts | 1 + .../test/common/response.test.ts | 12 ++++++ .../test/common/schemas/common.d.ts | 42 +++++++++++++++++++ .../test/common/schemas/common.yaml | 11 +++++ 6 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 .changeset/wet-waves-turn.md diff --git a/.changeset/wet-waves-turn.md b/.changeset/wet-waves-turn.md new file mode 100644 index 000000000..dd8f41d29 --- /dev/null +++ b/.changeset/wet-waves-turn.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": patch +--- + +Use text() when no content-length is provided to avoid errors parsing empty bodies (200 with no content) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index be3b153a3..ab87033b3 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -230,18 +230,29 @@ 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") { + if (response.status === 204 || request.method === "HEAD" || contentLength === "0") { return response.ok ? { data: undefined, response } : { error: undefined, response }; } // parse response (falling back to .text() when necessary) if (response.ok) { - // if "stream", skip parsing entirely - if (parseAs === "stream") { - return { data: response.body, response }; - } - return { data: await response[parseAs](), response }; + const getResponseData = async () => { + // if "stream", skip parsing entirely + if (parseAs === "stream") { + return response.body; + } + + if (parseAs === "json" && !contentLength) { + // use text() when no content-length is provided to avoid errors parsing empty bodies (200 with no content) + const raw = await response.text(); + return raw ? JSON.parse(raw) : undefined; + } + + return await response[parseAs](); + }; + return { data: await getResponseData(), response }; } // handle errors diff --git a/packages/openapi-fetch/test/common/request.test.ts b/packages/openapi-fetch/test/common/request.test.ts index 7aa87e563..6a836520e 100644 --- a/packages/openapi-fetch/test/common/request.test.ts +++ b/packages/openapi-fetch/test/common/request.test.ts @@ -342,6 +342,7 @@ describe("request", () => { clone: () => ({ ...response }), headers: new Headers(), json: async () => data, + text: async () => JSON.stringify(data), status: 200, ok: true, } as Response; diff --git a/packages/openapi-fetch/test/common/response.test.ts b/packages/openapi-fetch/test/common/response.test.ts index ca7d21497..cdedabc4c 100644 --- a/packages/openapi-fetch/test/common/response.test.ts +++ b/packages/openapi-fetch/test/common/response.test.ts @@ -138,6 +138,18 @@ describe("response", () => { describe("parseAs", () => { const client = createObservedClient({}, async () => Response.json({})); + test("json with empty response", async () => { + const client = createObservedClient({}, async () => new Response()); + const { data, error } = (await client.GET("/empty-json", { + parseAs: "json", + })) satisfies { data?: undefined }; + if (error) { + throw new Error("parseAs json: error"); + } + + expect(data).toBe(undefined); + }); + test("text", async () => { const { data, error } = (await client.GET("/resources", { parseAs: "text", diff --git a/packages/openapi-fetch/test/common/schemas/common.d.ts b/packages/openapi-fetch/test/common/schemas/common.d.ts index b684c117e..2b81c4f28 100644 --- a/packages/openapi-fetch/test/common/schemas/common.d.ts +++ b/packages/openapi-fetch/test/common/schemas/common.d.ts @@ -727,6 +727,22 @@ export interface paths { patch?: never; trace?: never; }; + "/empty-json": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getEmptyJsonResponse"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -779,4 +795,30 @@ export interface operations { }; }; }; + getEmptyJsonResponse: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Empty JSON Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; } diff --git a/packages/openapi-fetch/test/common/schemas/common.yaml b/packages/openapi-fetch/test/common/schemas/common.yaml index 72e1ea6e2..99bd4187d 100644 --- a/packages/openapi-fetch/test/common/schemas/common.yaml +++ b/packages/openapi-fetch/test/common/schemas/common.yaml @@ -421,6 +421,17 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /empty-json: + get: + operationId: getEmptyJsonResponse + responses: + 200: + description: Empty JSON Response + default: + content: + application/json: + schema: + $ref: "#/components/schemas/Error" components: schemas: Error: