From d0066ead4759eb391ab83efc44797a99f680a0b7 Mon Sep 17 00:00:00 2001 From: Joel Solano Date: Sat, 4 Oct 2025 17:05:52 +0200 Subject: [PATCH 1/4] fix: properly serialize url search params --- packages/better-fetch/src/fetch.ts | 2 +- packages/better-fetch/src/test/fetch.test.ts | 18 +++++++++--------- packages/better-fetch/src/url.ts | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/better-fetch/src/fetch.ts b/packages/better-fetch/src/fetch.ts index 1a8a61c..5959eee 100644 --- a/packages/better-fetch/src/fetch.ts +++ b/packages/better-fetch/src/fetch.ts @@ -40,7 +40,7 @@ export const betterFetch = async < const fetch = getFetch(opts); const controller = new AbortController(); const signal = opts.signal ?? controller.signal; - const _url = getURL(__url, opts); + const _url = await getURL(__url, opts); const body = getBody(opts); const headers = await getHeaders(opts); const method = getMethod(__url, opts); diff --git a/packages/better-fetch/src/test/fetch.test.ts b/packages/better-fetch/src/test/fetch.test.ts index 6c1e6ff..b1038df 100644 --- a/packages/better-fetch/src/test/fetch.test.ts +++ b/packages/better-fetch/src/test/fetch.test.ts @@ -437,7 +437,7 @@ describe("fetch-error-throw", () => { describe("url", () => { it("should work with params", async () => { - const url = getURL("param/:id", { + const url = await getURL("param/:id", { params: { id: "1", }, @@ -447,7 +447,7 @@ describe("url", () => { }); it("should use the url base if the url starts with http", async () => { - const url = getURL("http://localhost:4001/param/:id", { + const url = await getURL("http://localhost:4001/param/:id", { params: { id: "1", }, @@ -456,7 +456,7 @@ describe("url", () => { }); it("should work with query params", async () => { - const url = getURL("/query", { + const url = await getURL("/query", { query: { id: "1", }, @@ -466,7 +466,7 @@ describe("url", () => { }); it("should not include nullable values in query params", async () => { - const url = getURL("/query", { + const url = await getURL("/query", { query: { id: "1", nullValue: null, @@ -478,7 +478,7 @@ describe("url", () => { }); it("should work with dynamic params", async () => { - const url = getURL("/param/:id", { + const url = await getURL("/param/:id", { params: { id: "1", }, @@ -488,7 +488,7 @@ describe("url", () => { }); it("should merge query from the url", async () => { - const url = getURL("/query?name=test&age=20", { + const url = await getURL("/query?name=test&age=20", { query: { id: "1", }, @@ -500,7 +500,7 @@ describe("url", () => { }); it("should give priority based on the order", async () => { - const url = getURL("/query", { + const url = await getURL("/query", { query: { id: "1", name: "test2", @@ -511,7 +511,7 @@ describe("url", () => { }); it("should encode the query params", async () => { - const url = getURL("/query", { + const url = await getURL("/query", { query: { id: "#20", name: "test 2", @@ -524,7 +524,7 @@ describe("url", () => { }); it("should encode dynamic params", async () => { - const url = getURL("/param/:id/:space", { + const url = await getURL("/param/:id/:space", { params: { id: "#test", space: "item 1", diff --git a/packages/better-fetch/src/url.ts b/packages/better-fetch/src/url.ts index 4a8ad07..4247bdb 100644 --- a/packages/better-fetch/src/url.ts +++ b/packages/better-fetch/src/url.ts @@ -4,7 +4,7 @@ import { BetterFetchOption } from "./types"; /** * Normalize URL */ -export function getURL(url: string, option?: BetterFetchOption) { +export async function getURL(url: string, option?: BetterFetchOption) { let { baseURL, params, query } = option || { query: {}, params: {}, @@ -29,7 +29,7 @@ export function getURL(url: string, option?: BetterFetchOption) { const queryParams = new URLSearchParams(urlQuery); for (const [key, value] of Object.entries(query || {})) { if (value == null) continue; - queryParams.set(key, String(value)); + queryParams.set(key, typeof value === "string" ? value : JSON.stringify(value)); } if (params) { if (Array.isArray(params)) { From c11a2548b5e34414f12ec139de30d0072b0d4a8d Mon Sep 17 00:00:00 2001 From: Joel Solano Date: Sat, 4 Oct 2025 17:09:28 +0200 Subject: [PATCH 2/4] chore: remove unnecessary awaits --- packages/better-fetch/src/fetch.ts | 2 +- packages/better-fetch/src/test/fetch.test.ts | 18 +++++++++--------- packages/better-fetch/src/url.ts | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/better-fetch/src/fetch.ts b/packages/better-fetch/src/fetch.ts index 5959eee..1a8a61c 100644 --- a/packages/better-fetch/src/fetch.ts +++ b/packages/better-fetch/src/fetch.ts @@ -40,7 +40,7 @@ export const betterFetch = async < const fetch = getFetch(opts); const controller = new AbortController(); const signal = opts.signal ?? controller.signal; - const _url = await getURL(__url, opts); + const _url = getURL(__url, opts); const body = getBody(opts); const headers = await getHeaders(opts); const method = getMethod(__url, opts); diff --git a/packages/better-fetch/src/test/fetch.test.ts b/packages/better-fetch/src/test/fetch.test.ts index b1038df..6c1e6ff 100644 --- a/packages/better-fetch/src/test/fetch.test.ts +++ b/packages/better-fetch/src/test/fetch.test.ts @@ -437,7 +437,7 @@ describe("fetch-error-throw", () => { describe("url", () => { it("should work with params", async () => { - const url = await getURL("param/:id", { + const url = getURL("param/:id", { params: { id: "1", }, @@ -447,7 +447,7 @@ describe("url", () => { }); it("should use the url base if the url starts with http", async () => { - const url = await getURL("http://localhost:4001/param/:id", { + const url = getURL("http://localhost:4001/param/:id", { params: { id: "1", }, @@ -456,7 +456,7 @@ describe("url", () => { }); it("should work with query params", async () => { - const url = await getURL("/query", { + const url = getURL("/query", { query: { id: "1", }, @@ -466,7 +466,7 @@ describe("url", () => { }); it("should not include nullable values in query params", async () => { - const url = await getURL("/query", { + const url = getURL("/query", { query: { id: "1", nullValue: null, @@ -478,7 +478,7 @@ describe("url", () => { }); it("should work with dynamic params", async () => { - const url = await getURL("/param/:id", { + const url = getURL("/param/:id", { params: { id: "1", }, @@ -488,7 +488,7 @@ describe("url", () => { }); it("should merge query from the url", async () => { - const url = await getURL("/query?name=test&age=20", { + const url = getURL("/query?name=test&age=20", { query: { id: "1", }, @@ -500,7 +500,7 @@ describe("url", () => { }); it("should give priority based on the order", async () => { - const url = await getURL("/query", { + const url = getURL("/query", { query: { id: "1", name: "test2", @@ -511,7 +511,7 @@ describe("url", () => { }); it("should encode the query params", async () => { - const url = await getURL("/query", { + const url = getURL("/query", { query: { id: "#20", name: "test 2", @@ -524,7 +524,7 @@ describe("url", () => { }); it("should encode dynamic params", async () => { - const url = await getURL("/param/:id/:space", { + const url = getURL("/param/:id/:space", { params: { id: "#test", space: "item 1", diff --git a/packages/better-fetch/src/url.ts b/packages/better-fetch/src/url.ts index 4247bdb..282ce7a 100644 --- a/packages/better-fetch/src/url.ts +++ b/packages/better-fetch/src/url.ts @@ -4,7 +4,7 @@ import { BetterFetchOption } from "./types"; /** * Normalize URL */ -export async function getURL(url: string, option?: BetterFetchOption) { +export function getURL(url: string, option?: BetterFetchOption) { let { baseURL, params, query } = option || { query: {}, params: {}, From a01e4c215540b489fd3b86e2a85791c93c9fdde2 Mon Sep 17 00:00:00 2001 From: Joel Solano Date: Sat, 4 Oct 2025 17:11:56 +0200 Subject: [PATCH 3/4] chore: add tests --- packages/better-fetch/src/test/fetch.test.ts | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/better-fetch/src/test/fetch.test.ts b/packages/better-fetch/src/test/fetch.test.ts index 6c1e6ff..def71f0 100644 --- a/packages/better-fetch/src/test/fetch.test.ts +++ b/packages/better-fetch/src/test/fetch.test.ts @@ -533,4 +533,39 @@ describe("url", () => { }); expect(url.toString()).toBe("http://localhost:4001/param/%23test/item%201"); }); + + it("should preserve arrays as JSON strings", () => { + const url = getURL("/test", { + query: { + filterValue: ["admin", "user"], + }, + baseURL: "http://localhost:4000", + }); + + expect(url.toString()).toBe( + 'http://localhost:4000/test?filterValue=%5B%22admin%22%2C%22user%22%5D' + ); + }); + + it("should preserve objects as JSON strings", () => { + const url = getURL("/test", { + query: { + options: { page: 1, limit: 10 }, + }, + baseURL: "http://localhost:4000", + }); + + expect(url.toString()).toBe( + 'http://localhost:4000/test?options=%7B%22page%22%3A1%2C%22limit%22%3A10%7D' + ); + }); + + it("should leave strings untouched", () => { + const url = getURL("/test", { + query: { foo: "bar" }, + baseURL: "http://localhost:4000", + }); + + expect(url.toString()).toBe("http://localhost:4000/test?foo=bar"); + }); }); From 372a7c7d7e4ec78ccd7f22f912aa563ac9709a7f Mon Sep 17 00:00:00 2001 From: Joel Solano Date: Sat, 4 Oct 2025 18:16:18 +0200 Subject: [PATCH 4/4] refactir: expand array values into multiple query params --- packages/better-fetch/src/test/fetch.test.ts | 6 +++--- packages/better-fetch/src/url.ts | 21 ++++++++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/better-fetch/src/test/fetch.test.ts b/packages/better-fetch/src/test/fetch.test.ts index def71f0..cf23945 100644 --- a/packages/better-fetch/src/test/fetch.test.ts +++ b/packages/better-fetch/src/test/fetch.test.ts @@ -534,7 +534,7 @@ describe("url", () => { expect(url.toString()).toBe("http://localhost:4001/param/%23test/item%201"); }); - it("should preserve arrays as JSON strings", () => { + it("should expand array values into multiple query parameters", () => { const url = getURL("/test", { query: { filterValue: ["admin", "user"], @@ -543,7 +543,7 @@ describe("url", () => { }); expect(url.toString()).toBe( - 'http://localhost:4000/test?filterValue=%5B%22admin%22%2C%22user%22%5D' + "http://localhost:4000/test?filterValue=admin&filterValue=user", ); }); @@ -556,7 +556,7 @@ describe("url", () => { }); expect(url.toString()).toBe( - 'http://localhost:4000/test?options=%7B%22page%22%3A1%2C%22limit%22%3A10%7D' + "http://localhost:4000/test?options=%7B%22page%22%3A1%2C%22limit%22%3A10%7D", ); }); diff --git a/packages/better-fetch/src/url.ts b/packages/better-fetch/src/url.ts index 282ce7a..1f9af3d 100644 --- a/packages/better-fetch/src/url.ts +++ b/packages/better-fetch/src/url.ts @@ -1,11 +1,11 @@ import { methods } from "./create-fetch"; -import { BetterFetchOption } from "./types"; +import type { BetterFetchOption } from "./types"; /** * Normalize URL */ export function getURL(url: string, option?: BetterFetchOption) { - let { baseURL, params, query } = option || { + const { baseURL, params, query } = option || { query: {}, params: {}, baseURL: "", @@ -29,7 +29,18 @@ export function getURL(url: string, option?: BetterFetchOption) { const queryParams = new URLSearchParams(urlQuery); for (const [key, value] of Object.entries(query || {})) { if (value == null) continue; - queryParams.set(key, typeof value === "string" ? value : JSON.stringify(value)); + let serializedValue; + if (typeof value === "string") { + serializedValue = value; + } else if (Array.isArray(value)) { + for (const val of value) { + queryParams.append(key, val); + } + continue; + } else { + serializedValue = JSON.stringify(value); + } + queryParams.set(key, serializedValue); } if (params) { if (Array.isArray(params)) { @@ -49,7 +60,9 @@ export function getURL(url: string, option?: BetterFetchOption) { if (path.startsWith("/")) path = path.slice(1); let queryParamString = queryParams.toString(); queryParamString = - queryParamString.length > 0 ? `?${queryParamString}`.replace(/\+/g, "%20") : ""; + queryParamString.length > 0 + ? `?${queryParamString}`.replace(/\+/g, "%20") + : ""; if (!basePath.startsWith("http")) { return `${basePath}${path}${queryParamString}`; }