Skip to content

Commit 4e60a6f

Browse files
committed
fix: Automatically decode query/path params
1 parent dfa67b1 commit 4e60a6f

4 files changed

Lines changed: 42 additions & 39 deletions

File tree

benchmarks/overhead.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import { Elysia } from "elysia";
2+
import { Hono } from "hono";
3+
import { createMiddleware } from "hono/factory";
4+
import { Bench } from "tinybench";
5+
import { z } from "zod/v4";
6+
17
/**
28
* Run some targeted benchmarks aiming to compare the internal handlers of
39
* different frameworks.
@@ -7,13 +13,8 @@
713
* bun run benchmarks/overhead.ts
814
* bun run benchmarks/overhead.ts "{{benchmark_name}}"
915
*/
10-
import { createApp } from "@aklinker1/zeta";
11-
import type { ServerSideFetch } from "@aklinker1/zeta/types";
12-
import { Elysia } from "elysia";
13-
import { Hono } from "hono";
14-
import { createMiddleware } from "hono/factory";
15-
import { Bench } from "tinybench";
16-
import { z } from "zod/v4";
16+
import { createApp } from "../dist/index.mjs";
17+
import type { ServerSideFetch } from "../dist/types.mjs";
1718

1819
process.env.NODE_ENV = "production";
1920

src/__tests__/app.test.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,6 @@ describe("App", () => {
194194
input: { a: "a" },
195195
expected: { a: "a" },
196196
},
197-
// {
198-
// input: { a: ["a", "b"] },
199-
// expected: { a: ["a", "b"] },
200-
// },
201-
// {
202-
// input: { a: "a", b: "b" },
203-
// expected: { a: "a", b: "b" },
204-
// },
205197
])("should parse the query parameters correctly for: %j", async ({ input, expected }) => {
206198
let actual: any;
207199
const app = createApp().get(
@@ -241,7 +233,26 @@ describe("App", () => {
241233
});
242234

243235
expect(actual).toEqual({ limit: 50 });
244-
expect(typeof actual.limit).toBe("number");
236+
});
237+
238+
it("should decode query parameters correctly", async () => {
239+
let actual: any;
240+
const app = createApp().get(
241+
"/test",
242+
{
243+
query: z.object({
244+
text: z.string(),
245+
}),
246+
},
247+
({ query }) => void (actual = query),
248+
);
249+
const client = createTestAppClient(app);
250+
251+
await client.fetch("GET", "/test", {
252+
query: { text: "hello world" },
253+
});
254+
255+
expect(actual).toEqual({ text: "hello world" });
245256
});
246257
});
247258

src/client.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,6 @@ export function createAppClient<TApp extends App>(
5151
): AppClient<GetClientRoutes<TApp>> {
5252
const { baseUrl = location.origin, fetch = globalThis.fetch, headers = {} } = options ?? {};
5353

54-
const buildSearchParams = (query: Record<string, unknown>) => {
55-
return new URLSearchParams(
56-
Object.entries(query)
57-
.filter(([, value]) => value != null)
58-
.map(([key, value]) => [key, String(value)]),
59-
).toString();
60-
};
61-
6254
const buildPath = (route: string, params: Record<string, unknown>) => {
6355
return Object.entries(params).reduce(
6456
(path, [key, value]) =>
@@ -72,10 +64,13 @@ export function createAppClient<TApp extends App>(
7264

7365
return {
7466
async fetch(method: string, route: string, inputs: any) {
75-
const searchParams =
76-
inputs.query == null ? "" : `?${buildSearchParams(inputs.query).toString()}`;
77-
const path = inputs.params == null ? route : buildPath(route, inputs.params);
78-
const url = `${join(baseUrl, path)}${searchParams}`;
67+
const pathname = inputs.params ? buildPath(route, inputs.params) : route;
68+
const url = new URL(pathname, baseUrl);
69+
if (inputs.query) {
70+
for (const [key, value] of Object.entries(inputs.query)) {
71+
url.searchParams.set(key, String(value));
72+
}
73+
}
7974

8075
const init = {
8176
body: undefined as BodyInit | undefined,
@@ -152,11 +147,3 @@ export type CreateAppClientOptions = {
152147
*/
153148
headers?: Record<string, string>;
154149
};
155-
156-
/** Join string together using `/` without double slashes. */
157-
function join(...paths: string[]) {
158-
return paths
159-
.map((path) => path.replace(/^\/+|\/+$/g, ""))
160-
.filter(Boolean)
161-
.join("/");
162-
}

src/internal/utils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ export function getRawPathname(request: Request): string {
5050
// Find end of pathname (before ? or #)
5151
for (let i = start + 1; i < request.url.length; i++) {
5252
if (request.url[i] === "?" || request.url[i] === "#") {
53-
return request.url.slice(start, i);
53+
return decodeUrlString(request.url.slice(start, i));
5454
}
5555
}
56-
return request.url.slice(start);
56+
return decodeUrlString(request.url.slice(start));
5757
}
5858

5959
export function getRawQuery(request: Request): Record<string, string> {
@@ -70,7 +70,7 @@ export function getRawQuery(request: Request): Record<string, string> {
7070
const end = i === len - 1 ? len : i;
7171
const eqIndex = str.indexOf("=", start);
7272
if (eqIndex !== -1 && eqIndex < end) {
73-
res[str.slice(start, eqIndex)] = str.slice(eqIndex + 1, end);
73+
res[str.slice(start, eqIndex)] = decodeUrlString(str.slice(eqIndex + 1, end));
7474
}
7575
start = i + 1;
7676
}
@@ -158,3 +158,7 @@ export function cleanupCompiledWhitespace(code: string): string {
158158
.replaceAll("{\n\n", "{\n")
159159
);
160160
}
161+
162+
function decodeUrlString(text: string): string {
163+
return decodeURIComponent(text.replaceAll("+", " "));
164+
}

0 commit comments

Comments
 (0)