From 3a90e597245b328df04923912af1fa8ab44ea5a3 Mon Sep 17 00:00:00 2001 From: "nedunchezhiyan.m" Date: Mon, 13 Apr 2026 19:40:10 +0530 Subject: [PATCH 1/4] fix(openapi-fetch): pass string bodies through without JSON serialization defaultBodySerializer was calling JSON.stringify on all non-FormData bodies, including strings that are already serialized. This caused string values to gain an extra layer of JSON quoting (e.g. "hello" became '"hello"'). Add an early return for string bodies so they are sent as-is. Fixes #2555 --- packages/openapi-fetch/src/index.js | 3 +++ packages/openapi-fetch/test/common/request.test.ts | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 4c7250fb0..ebfea6c0c 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -625,6 +625,9 @@ export function defaultBodySerializer(body, headers) { if (body instanceof FormData) { return body; } + if (typeof body === "string") { + return body; + } if (headers) { const contentType = headers.get instanceof Function diff --git a/packages/openapi-fetch/test/common/request.test.ts b/packages/openapi-fetch/test/common/request.test.ts index 6a836520e..be926327a 100644 --- a/packages/openapi-fetch/test/common/request.test.ts +++ b/packages/openapi-fetch/test/common/request.test.ts @@ -236,7 +236,19 @@ describe("request", () => { }); expect(bodyUsed).toBe(true); - expect(bodyText).toBe('""'); + expect(bodyText).toBe(""); + }); + + test.each(BODY_ACCEPTING_METHODS)("string body is passed through without JSON serialization - %s", async (method) => { + const { bodyUsed, bodyText } = await fireRequestAndGetBodyInformation({ + method, + fetchOptions: { + body: "pre-serialized string", + }, + }); + + expect(bodyUsed).toBe(true); + expect(bodyText).toBe("pre-serialized string"); }); test.each(BODY_ACCEPTING_METHODS)("`0` body (with body serializer) - %s", async (method) => { From fb5557c7586d1720fad068187a988c7cb1443cc5 Mon Sep 17 00:00:00 2001 From: "nedunchezhiyan.m" Date: Mon, 13 Apr 2026 19:44:09 +0530 Subject: [PATCH 2/4] fix(openapi-fetch): return truthy error for empty error response bodies When a server returns an error status (4xx/5xx) with no body, the error field was undefined, causing if (error) checks to silently pass. Now returns the HTTP status text (e.g. "Not Found") or status code as a string (e.g. "500") so error detection works consistently regardless of response body presence. Fixes #2530 --- packages/openapi-fetch/src/index.js | 5 ++--- .../test/http-methods/delete.test.ts | 21 +++++++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index ebfea6c0c..3df462fff 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -242,7 +242,7 @@ export default function createClient(clientOptions) { request.method === "HEAD" || (contentLength === "0" && !response.headers.get("Transfer-Encoding")?.includes("chunked")) ) { - return response.ok ? { data: undefined, response } : { error: undefined, response }; + return response.ok ? { data: undefined, response } : { error: response.statusText || String(response.status), response }; } // parse response (falling back to .text() when necessary) @@ -267,8 +267,7 @@ export default function createClient(clientOptions) { // handle errors (use text() when no content-length to safely handle empty bodies from proxies) const raw = await response.text(); if (!raw) { - // empty error body - return undefined to be consistent with status 204 handling - return { error: undefined, response }; + return { error: response.statusText || String(response.status), response }; } let error = raw; try { diff --git a/packages/openapi-fetch/test/http-methods/delete.test.ts b/packages/openapi-fetch/test/http-methods/delete.test.ts index 16979ca7a..24ed10bb1 100644 --- a/packages/openapi-fetch/test/http-methods/delete.test.ts +++ b/packages/openapi-fetch/test/http-methods/delete.test.ts @@ -62,14 +62,31 @@ describe("DELETE", () => { // assert data is undefined for error response expect(data).toBeUndefined(); - // assert error is undefined for empty body (consistent with 204 and Content-Length: 0 handling) - expect(error).toBeUndefined(); + // assert error is truthy for error response with empty body + expect(error).toBe("500"); // assert response status is preserved expect(response.status).toBe(500); expect(response.ok).toBe(false); }); + test("error response with Content-Length: 0 returns truthy error", async () => { + const client = createObservedClient( + {}, + async () => new Response(null, { status: 404, statusText: "Not Found", headers: { "Content-Length": "0" } }), + ); + const { data, error, response } = await client.DELETE("/tags/{name}", { + params: { + path: { name: "Tag" }, + }, + }); + + expect(data).toBeUndefined(); + expect(error).toBe("Not Found"); + expect(response.status).toBe(404); + expect(response.ok).toBe(false); + }); + test("handles success response with empty body when Content-Length header is stripped by proxy", async () => { // Simulate proxy stripping Content-Length header from an empty success response const client = createObservedClient( From 6a7d18e288fc77a26c43a3bf85a11027a168bd7d Mon Sep 17 00:00:00 2001 From: Nedunchezhiyan-M Date: Mon, 13 Apr 2026 20:41:44 +0530 Subject: [PATCH 3/4] chore: add changeset for empty error body fix --- .changeset/fix-empty-error-body-truthy.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-empty-error-body-truthy.md diff --git a/.changeset/fix-empty-error-body-truthy.md b/.changeset/fix-empty-error-body-truthy.md new file mode 100644 index 000000000..f31b797c1 --- /dev/null +++ b/.changeset/fix-empty-error-body-truthy.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": patch +--- + +Fix `error` being `undefined` when a server returns a 4xx/5xx status with no response body. Previously `if (error)` checks would silently pass even on error responses. The `error` field now contains the HTTP status text (e.g. `"Not Found"`) or status code string (e.g. `"500"`) when the body is empty, making `if (error)` reliable for all error responses. From 0b6d29ff1be60110817edee54034907d89a1b77c Mon Sep 17 00:00:00 2001 From: Nedunchezhiyan-M Date: Wed, 15 Apr 2026 00:31:00 +0530 Subject: [PATCH 4/4] fix(openapi-fetch): resolve biome lint errors Co-Authored-By: Claude Sonnet 4.6 --- packages/openapi-fetch/biome.json | 2 +- packages/openapi-fetch/src/index.js | 2 +- packages/openapi-fetch/test/common/create-client-e2e.test.js | 2 +- packages/openapi-fetch/test/common/response.test.ts | 2 +- packages/openapi-fetch/test/e2e/index.e2e.ts | 2 +- packages/openapi-fetch/test/http-methods/get.test.ts | 2 +- packages/openapi-fetch/test/middleware/middleware.test.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/openapi-fetch/biome.json b/packages/openapi-fetch/biome.json index 99724ae16..f32a6de1e 100644 --- a/packages/openapi-fetch/biome.json +++ b/packages/openapi-fetch/biome.json @@ -3,7 +3,7 @@ "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", "extends": "//", "files": { - "includes": ["src/**", "test/**", "!**/examples/**", "!test/**/schemas/**", "!test/bench/**/*.min.js"] + "includes": ["src/**", "test/**", "!**/examples", "!test/**/schemas", "!test/bench/**/*.min.js"] }, "linter": { "rules": { diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 3df462fff..5df9636cf 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -4,7 +4,7 @@ const PATH_PARAM_RE = /\{[^{}]+\}/g; const supportsRequestInitExt = () => { return ( typeof process === "object" && - Number.parseInt(process?.versions?.node?.substring(0, 2)) >= 18 && + Number.parseInt(process?.versions?.node?.substring(0, 2), 10) >= 18 && process.versions.undici ); }; diff --git a/packages/openapi-fetch/test/common/create-client-e2e.test.js b/packages/openapi-fetch/test/common/create-client-e2e.test.js index 88536e083..51fe78f6d 100644 --- a/packages/openapi-fetch/test/common/create-client-e2e.test.js +++ b/packages/openapi-fetch/test/common/create-client-e2e.test.js @@ -67,7 +67,7 @@ const caToBuffer = (ca) => { const API_PORT = process.env.API_PORT || 4578; const app = express(); -app.get("/v1/foo", (req, res) => { +app.get("/v1/foo", (_req, res) => { res.send("bar"); }); diff --git a/packages/openapi-fetch/test/common/response.test.ts b/packages/openapi-fetch/test/common/response.test.ts index 72f4f1f6e..0144cda10 100644 --- a/packages/openapi-fetch/test/common/response.test.ts +++ b/packages/openapi-fetch/test/common/response.test.ts @@ -127,7 +127,7 @@ describe("response", () => { describe("response object", () => { test.each([200, 404, 500] as const)("%s", async (status) => { - const client = createObservedClient({}, async (req) => + const client = createObservedClient({}, async (_req) => Response.json({ status, message: "OK" }, { status }), ); const result = await client.GET(status === 200 ? "/resources" : `/error-${status}`); diff --git a/packages/openapi-fetch/test/e2e/index.e2e.ts b/packages/openapi-fetch/test/e2e/index.e2e.ts index ecd6cfaa5..38a371704 100644 --- a/packages/openapi-fetch/test/e2e/index.e2e.ts +++ b/packages/openapi-fetch/test/e2e/index.e2e.ts @@ -1,4 +1,4 @@ -import { expect, type Page, test } from "@playwright/test"; +import { type Page, test } from "@playwright/test"; // note: these tests load Chrome, Firefox, and Safari in Playwright to test a browser-realistic runtime. // the frontend is prepared via Vite to create a production-accurate app (and throw add’l type errors) diff --git a/packages/openapi-fetch/test/http-methods/get.test.ts b/packages/openapi-fetch/test/http-methods/get.test.ts index 991f72453..36ad107a4 100644 --- a/packages/openapi-fetch/test/http-methods/get.test.ts +++ b/packages/openapi-fetch/test/http-methods/get.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; import { createObservedClient } from "../helpers.js"; -import type { components, paths } from "./schemas/get.js"; +import type { paths } from "./schemas/get.js"; describe("GET", () => { test("sends correct method", async () => { diff --git a/packages/openapi-fetch/test/middleware/middleware.test.ts b/packages/openapi-fetch/test/middleware/middleware.test.ts index 0a1fecdea..4ad25ad9c 100644 --- a/packages/openapi-fetch/test/middleware/middleware.test.ts +++ b/packages/openapi-fetch/test/middleware/middleware.test.ts @@ -98,7 +98,7 @@ test("can modify response", async () => { test("returns original errors if nothing is returned", async () => { const actualError = new Error(); - const client = createObservedClient({}, async (req) => { + const client = createObservedClient({}, async (_req) => { throw actualError; }); client.use({