From 538089f97eb89ffaa059efc408be4e035103406d Mon Sep 17 00:00:00 2001 From: Taesu Date: Sat, 5 Sep 2026 04:38:14 +0900 Subject: [PATCH 1/5] fix(types): allow omitted defaulted queries --- .../better-fetch/src/create-fetch/types.ts | 2 +- packages/better-fetch/src/test/create.test.ts | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/better-fetch/src/create-fetch/types.ts b/packages/better-fetch/src/create-fetch/types.ts index 8ef7293..33b1928 100644 --- a/packages/better-fetch/src/create-fetch/types.ts +++ b/packages/better-fetch/src/create-fetch/types.ts @@ -86,7 +86,7 @@ export type InferHeaders = H extends StandardSchemaV1 : Record; export type IsFieldOptional = T extends StandardSchemaV1 - ? undefined extends T + ? undefined extends StandardSchemaV1.InferInput ? true : false : true; diff --git a/packages/better-fetch/src/test/create.test.ts b/packages/better-fetch/src/test/create.test.ts index 9659002..c162142 100644 --- a/packages/better-fetch/src/test/create.test.ts +++ b/packages/better-fetch/src/test/create.test.ts @@ -182,6 +182,28 @@ describe("create-fetch-runtime-test", () => { }); }); + it("applies a default query when options are omitted", async () => { + let requestQuery: unknown; + const fetch = createFetch({ + baseURL: "https://example.com", + schema: createSchema({ + "/movies": { + query: z + .object({ include: z.array(z.string()) }) + .default({ include: ["recommendations"] }), + }, + }), + customFetchImpl: async () => new Response(), + onRequest(context) { + requestQuery = context.query; + }, + }); + + await fetch("/movies"); + + expect(requestQuery).toEqual({ include: ["recommendations"] }); + }); + it("should validate response and return data if validation passes", async () => { const res = await $fetch("/echo", { output: z.object({ From 5e1d36fb676795f13161b380c7de82b0a09da2fc Mon Sep 17 00:00:00 2001 From: Taesu Date: Sat, 5 Sep 2026 07:02:19 +0900 Subject: [PATCH 2/5] fix(fetch): preserve omitted headers for validation --- .../better-fetch/src/create-fetch/index.ts | 14 +++++++----- .../better-fetch/src/create-fetch/schema.ts | 5 ++++- packages/better-fetch/src/test/create.test.ts | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/better-fetch/src/create-fetch/index.ts b/packages/better-fetch/src/create-fetch/index.ts index cbca994..fe182d0 100644 --- a/packages/better-fetch/src/create-fetch/index.ts +++ b/packages/better-fetch/src/create-fetch/index.ts @@ -58,14 +58,18 @@ export const applySchemaPlugin = (config: CreateFetchOption) => const validated = (await parseStandardSchema( keySchema.headers, - normalizedHeaders, - )) as Record; + Object.keys(normalizedHeaders).length > 0 + ? normalizedHeaders + : undefined, + )) as Record | undefined; const finalHeaders: Record = {}; - for (const [key, value] of Object.entries(validated)) { - finalHeaders[key.toLowerCase()] = value; + if (validated) { + for (const [key, value] of Object.entries(validated)) { + finalHeaders[key.toLowerCase()] = value; + } } - validatedHeaders = finalHeaders; + validatedHeaders = validated ? finalHeaders : undefined; } const opts = { diff --git a/packages/better-fetch/src/create-fetch/schema.ts b/packages/better-fetch/src/create-fetch/schema.ts index 8db973e..28145fa 100644 --- a/packages/better-fetch/src/create-fetch/schema.ts +++ b/packages/better-fetch/src/create-fetch/schema.ts @@ -7,7 +7,10 @@ export type FetchSchema = { query?: StandardSchemaV1; params?: StandardSchemaV1> | undefined; method?: Methods; - headers?: StandardSchemaV1>; + headers?: StandardSchemaV1< + Record | undefined, + Record | undefined + >; }; export type Methods = "get" | "post" | "put" | "patch" | "delete"; diff --git a/packages/better-fetch/src/test/create.test.ts b/packages/better-fetch/src/test/create.test.ts index 36c75a8..57213b4 100644 --- a/packages/better-fetch/src/test/create.test.ts +++ b/packages/better-fetch/src/test/create.test.ts @@ -205,6 +205,28 @@ describe("create-fetch-runtime-test", () => { expect(requestQuery).toEqual({ include: ["recommendations"] }); }); + it("applies default headers when options are omitted", async () => { + let requestHeader: string | null = null; + const fetch = createFetch({ + baseURL: "https://example.com", + schema: createSchema({ + "/users": { + headers: z + .object({ "x-api-version": z.string() }) + .default({ "x-api-version": "1" }), + }, + }), + customFetchImpl: async () => new Response(), + onRequest(context) { + requestHeader = context.headers.get("x-api-version"); + }, + }); + + await fetch("/users"); + + expect(requestHeader).toBe("1"); + }); + it("should validate response and return data if validation passes", async () => { const res = await $fetch("/echo", { output: z.object({ From c5ee1749f9eba7c75b9b172fbc20482c738993c7 Mon Sep 17 00:00:00 2001 From: Taesu Date: Sat, 5 Sep 2026 09:10:48 +0900 Subject: [PATCH 3/5] fix(fetch): preserve explicit empty headers --- .../better-fetch/src/create-fetch/index.ts | 56 +++++++++---------- packages/better-fetch/src/test/create.test.ts | 23 ++++++++ 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/packages/better-fetch/src/create-fetch/index.ts b/packages/better-fetch/src/create-fetch/index.ts index fe182d0..19f986f 100644 --- a/packages/better-fetch/src/create-fetch/index.ts +++ b/packages/better-fetch/src/create-fetch/index.ts @@ -4,6 +4,20 @@ import type { BetterFetchOption } from "../types"; import { mergeHeaders, parseStandardSchema } from "../utils"; import type { BetterFetch, CreateFetchOption } from "./types"; +const normalizeHeaders = ( + headers: BetterFetchOption["headers"], +): Record | undefined => { + if (headers === undefined) { + return undefined; + } + + const normalizedHeaders: Record = {}; + for (const [key, value] of Object.entries(mergeHeaders(headers))) { + normalizedHeaders[key.toLowerCase()] = value; + } + return normalizedHeaders; +}; + export const applySchemaPlugin = (config: CreateFetchOption) => ({ id: "apply-schema", @@ -41,35 +55,11 @@ export const applySchemaPlugin = (config: CreateFetchOption) => if (keySchema) { let validatedHeaders = options?.headers; if (keySchema.headers && !options?.disableValidation) { - const normalizedHeaders: Record = {}; - if (options?.headers) { - if (options.headers instanceof Headers) { - options.headers.forEach((value, key) => { - normalizedHeaders[key.toLowerCase()] = value; - }); - } else if (typeof options.headers === "object") { - for (const [key, value] of Object.entries(options.headers)) { - if (value !== null && value !== undefined) { - normalizedHeaders[key.toLowerCase()] = value; - } - } - } - } - const validated = (await parseStandardSchema( keySchema.headers, - Object.keys(normalizedHeaders).length > 0 - ? normalizedHeaders - : undefined, + normalizeHeaders(options?.headers), )) as Record | undefined; - - const finalHeaders: Record = {}; - if (validated) { - for (const [key, value] of Object.entries(validated)) { - finalHeaders[key.toLowerCase()] = value; - } - } - validatedHeaders = validated ? finalHeaders : undefined; + validatedHeaders = normalizeHeaders(validated); } const opts = { @@ -80,9 +70,6 @@ export const applySchemaPlugin = (config: CreateFetchOption) => ...(keySchema.output !== undefined && { output: keySchema.output, }), - ...(validatedHeaders !== undefined && { - headers: validatedHeaders, - }), ...(!options?.disableValidation && { body: keySchema.input ? await parseStandardSchema(keySchema.input, options?.body) @@ -95,6 +82,11 @@ export const applySchemaPlugin = (config: CreateFetchOption) => : options?.query, }), }; + if (validatedHeaders === undefined) { + delete opts.headers; + } else { + opts.headers = validatedHeaders; + } return { url, options: opts, @@ -112,10 +104,14 @@ export const createFetch =