From 67b26c6aa6dd58151f4894b1a109c666f6d20c49 Mon Sep 17 00:00:00 2001 From: greymoth <246701683+greymoth-jp@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:45:25 +0900 Subject: [PATCH] fix: normalize `headers` reassigned in an `onRequest` hook An `onRequest` hook can reassign `options.headers` to a plain object, but the request flow calls `.get()` on it right after the hook runs, throwing `headers.get is not a function` for payload requests. Re-normalize it to a `Headers` instance after the hook, mirroring the v1 fix in #524. --- src/fetch.ts | 8 ++++++-- test/index.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index 10c91583..4811f892 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -109,6 +109,12 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { if (context.options.onRequest) { await callHooks(context, context.options.onRequest); + // A hook may reassign `headers` to a plain object or array (a valid + // `HeadersInit`), so normalize it back to a `Headers` instance before the + // code below calls `.get()`/`.set()`/`.has()` on it. + if (!(context.options.headers instanceof Headers)) { + context.options.headers = new Headers(context.options.headers || {}); + } } if (typeof context.request === "string") { @@ -143,8 +149,6 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { // Set Content-Type and Accept headers to application/json by default // for JSON serializable request bodies. - // Pass empty object as older browsers don't support undefined. - context.options.headers = new Headers(context.options.headers || {}); if (!contentType) { context.options.headers.set("content-type", "application/json"); } diff --git a/test/index.test.ts b/test/index.test.ts index 5ac20b07..15520cc3 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -397,6 +397,32 @@ describe("ofetch", () => { ).toMatchObject({ foo: "2", bar: "3" }); }); + it("normalizes headers reassigned in onRequest hook", async () => { + // A `Headers` instance reassigned in the hook is sent as-is. + expect( + await $fetch(getURL("/echo"), { + method: "POST", + body: { num: 42 }, + onRequest(ctx) { + ctx.options.headers = new Headers({ "x-foo": "bar" }); + }, + }).then((r) => r.headers) + ).toMatchObject({ "x-foo": "bar" }); + + // A plain object reassigned in the hook is normalized back to `Headers` + // instead of throwing `headers.get is not a function` before the request. + expect( + await $fetch(getURL("/echo"), { + method: "POST", + body: { num: 42 }, + onRequest(ctx) { + // @ts-expect-error backwards compatibility for object headers + ctx.options.headers = { "x-foo": "bar" }; + }, + }).then((r) => r.headers) + ).toMatchObject({ "x-foo": "bar" }); + }); + it("hook errors", async () => { // onRequest await expect(