From 3a90e597245b328df04923912af1fa8ab44ea5a3 Mon Sep 17 00:00:00 2001 From: "nedunchezhiyan.m" Date: Mon, 13 Apr 2026 19:40:10 +0530 Subject: [PATCH 1/3] 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 124c63d1377075717996684e9c01a9d834f95831 Mon Sep 17 00:00:00 2001 From: Nedunchezhiyan-M Date: Mon, 13 Apr 2026 20:41:20 +0530 Subject: [PATCH 2/3] chore: add changeset for string body serializer fix --- .changeset/fix-string-body-serializer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-string-body-serializer.md diff --git a/.changeset/fix-string-body-serializer.md b/.changeset/fix-string-body-serializer.md new file mode 100644 index 000000000..0139c95c1 --- /dev/null +++ b/.changeset/fix-string-body-serializer.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": patch +--- + +Fix `defaultBodySerializer` double-encoding string bodies. Previously, passing a string as the request body (e.g. a pre-serialized JSON string) would result in it being wrapped in an extra layer of JSON quotes. Strings are now passed through as-is. From 526e209f1402373329939ef3885ab734073535ba Mon Sep 17 00:00:00 2001 From: Nedunchezhiyan-M Date: Wed, 15 Apr 2026 00:29:17 +0530 Subject: [PATCH 3/3] 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 ebfea6c0c..7b37d7fc4 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({