diff --git a/src/fetch.ts b/src/fetch.ts index 10c91583..33be9040 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -115,9 +115,15 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { if (context.options.baseURL) { context.request = withBase(context.request, context.options.baseURL); } + // Merge params into query (params may be set in onRequest hook) + if (context.options.params) { + context.options.query = { + ...context.options.query, + ...context.options.params, + }; + } if (context.options.query) { context.request = withQuery(context.request, context.options.query); - delete context.options.query; } if ("query" in context.options) { delete context.options.query; diff --git a/src/utils.ts b/src/utils.ts index a773431b..af728a3c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -124,7 +124,7 @@ function mergeHeaders( Headers: typeof globalThis.Headers ): Headers { if (!defaults) { - return new Headers(input); + return input ? new Headers(input) : new Headers(); } const headers = new Headers(defaults); if (input) { diff --git a/src/utils.url.ts b/src/utils.url.ts index 5a725353..1e88ebaa 100644 --- a/src/utils.url.ts +++ b/src/utils.url.ts @@ -38,6 +38,10 @@ export function joinURL(base?: string, path?: string): string { * Adds the base path to the input path, if it is not already present. */ export function withBase(input = "", base = ""): string { + // Trim control characters and whitespace from base URL + // (can happen from .env parsing mistakes) + base = base.trim(); + if (!base || base === "/") { return input; } diff --git a/test/index.test.ts b/test/index.test.ts index 5ac20b07..f202e1ba 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -524,4 +524,45 @@ describe("ofetch", () => { timeout: 10_000, }); }); + + describe("bugfixes", () => { + it("handles undefined headers without crash (#493)", async () => { + // Should not throw when no headers are provided + const result = await $fetch(getURL("ok"), {}); + expect(result).toBe("ok"); + }); + + it("merges params set in onRequest hook (#477)", async () => { + const { path } = await $fetch(getURL("echo"), { + query: { a: 1 }, + onRequest(ctx) { + ctx.options.params = { b: 2 }; + }, + }); + const params = Object.fromEntries(new URL(path, "http://_").searchParams); + expect(params).toMatchObject({ a: "1", b: "2" }); + }); + + it("merges query set in onRequest hook", async () => { + const { path } = await $fetch(getURL("echo"), { + onRequest(ctx) { + ctx.options.query = { dynamic: "true" }; + }, + }); + const params = Object.fromEntries(new URL(path, "http://_").searchParams); + expect(params).toMatchObject({ dynamic: "true" }); + }); + + it("trims whitespace from baseURL (#530)", async () => { + const result = await $fetch("/x?foo=123", { + baseURL: getURL("url") + " ", + }); + expect(result).toBe("/url/x?foo=123"); + + const result2 = await $fetch("/x?foo=123", { + baseURL: getURL("url") + "\t", + }); + expect(result2).toBe("/url/x?foo=123"); + }); + }); });