Skip to content

Commit 24b9271

Browse files
pi0claude
andcommitted
feat(utils): add requestWithURL and requestWithBaseURL utils
Lightweight request proxying via `Proxy` to override the URL without cloning the entire `Request` object. Uses a shared proxy handler with a `WeakMap` for per-proxy URL storage to minimize allocations. `requestWithBaseURL` strips a base path prefix from the request URL and is now used internally by `H3.mount()` instead of `new Request()`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 60a2e91 commit 24b9271

File tree

5 files changed

+113
-3
lines changed

5 files changed

+113
-3
lines changed

src/h3.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createRouter, addRoute, findRoute } from "rou3";
22
import { H3Event } from "./event.ts";
33
import { toResponse, kNotFound } from "./response.ts";
44
import { callMiddleware, normalizeMiddleware } from "./middleware.ts";
5+
import { requestWithBaseURL } from "./utils/request.ts";
56

67
import type { ServerRequest } from "srvx";
78
import type { H3Config, H3CoreConfig, H3Plugin, MatchedRoute, RouterContext } from "./types/h3.ts";
@@ -142,9 +143,7 @@ export const H3 = /* @__PURE__ */ (() => {
142143
} else {
143144
const fetchHandler = "fetch" in input ? input.fetch : input;
144145
this.all(`${base}/**`, function _mountedMiddleware(event) {
145-
const url = new URL(event.url);
146-
url.pathname = url.pathname.slice(base.length) || "/";
147-
return fetchHandler(new Request(url, event.req));
146+
return fetchHandler(requestWithBaseURL(event.req, base));
148147
});
149148
}
150149
return this;

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export { type RouteDefinition, defineRoute } from "./utils/route.ts";
8282
// Request
8383

8484
export {
85+
requestWithURL,
86+
requestWithBaseURL,
8587
toRequest,
8688
getRequestHost,
8789
getRequestIP,

src/utils/request.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,36 @@ import type { HTTPMethod } from "../types/h3.ts";
1111
import type { H3EventContext } from "../types/context.ts";
1212
import type { ServerRequest } from "srvx";
1313

14+
const _urlOverrides = new WeakMap<Request, string>();
15+
16+
const _proxyHandler: ProxyHandler<Request> = {
17+
get(target, prop, receiver) {
18+
if (prop === "url") return _urlOverrides.get(receiver);
19+
const value = Reflect.get(target, prop);
20+
return typeof value === "function" ? value.bind(target) : value;
21+
},
22+
};
23+
24+
/**
25+
* Create a lightweight request proxy that overrides only the URL.
26+
*
27+
* Avoids cloning the original request (no `new Request()` allocation).
28+
*/
29+
export function requestWithURL(req: ServerRequest, url: string): ServerRequest {
30+
const proxy = new Proxy(req, _proxyHandler);
31+
_urlOverrides.set(proxy, url);
32+
return proxy;
33+
}
34+
35+
/**
36+
* Create a lightweight request proxy with the base path stripped from the URL pathname.
37+
*/
38+
export function requestWithBaseURL(req: ServerRequest, base: string): ServerRequest {
39+
const url = new URL(req.url);
40+
url.pathname = url.pathname.slice(base.length) || "/";
41+
return requestWithURL(req, url.href);
42+
}
43+
1444
/**
1545
* Convert input into a web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).
1646
*

test/unit/package.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ describe("h3 package", () => {
106106
"readValidatedBody",
107107
"redirect",
108108
"removeResponseHeader",
109+
"requestWithBaseURL",
110+
"requestWithURL",
109111
"requireBasicAuth",
110112
"sanitizeStatusCode",
111113
"sanitizeStatusMessage",

test/unit/request.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, expect, it } from "vitest";
2+
import { requestWithURL, requestWithBaseURL } from "../../src/utils/request.ts";
3+
4+
describe("requestWithURL", () => {
5+
const original = new Request("http://example.com/base/path", {
6+
method: "POST",
7+
headers: { "x-test": "value" },
8+
body: "hello",
9+
});
10+
11+
it("overrides url", () => {
12+
const proxied = requestWithURL(original, "http://example.com/path");
13+
expect(proxied.url).toBe("http://example.com/path");
14+
});
15+
16+
it("preserves original url on source request", () => {
17+
requestWithURL(original, "http://example.com/path");
18+
expect(original.url).toBe("http://example.com/base/path");
19+
});
20+
21+
it("preserves method", () => {
22+
const proxied = requestWithURL(original, "http://example.com/path");
23+
expect(proxied.method).toBe("POST");
24+
});
25+
26+
it("preserves headers", () => {
27+
const proxied = requestWithURL(original, "http://example.com/path");
28+
expect(proxied.headers.get("x-test")).toBe("value");
29+
});
30+
31+
it("is instanceof Request", () => {
32+
const proxied = requestWithURL(original, "http://example.com/path");
33+
expect(proxied instanceof Request).toBe(true);
34+
});
35+
36+
it("clone() works and keeps overridden url", () => {
37+
const proxied = requestWithURL(original, "http://example.com/path");
38+
const cloned = proxied.clone();
39+
expect(cloned.url).toBe("http://example.com/base/path");
40+
expect(cloned.method).toBe("POST");
41+
});
42+
});
43+
44+
describe("requestWithBaseURL", () => {
45+
const original = new Request("http://example.com/base/path?q=1", {
46+
method: "POST",
47+
headers: { "x-test": "value" },
48+
body: "hello",
49+
});
50+
51+
it("strips base from pathname", () => {
52+
const proxied = requestWithBaseURL(original, "/base");
53+
expect(proxied.url).toBe("http://example.com/path?q=1");
54+
});
55+
56+
it("returns / when pathname equals base", () => {
57+
const req = new Request("http://example.com/base");
58+
const proxied = requestWithBaseURL(req, "/base");
59+
expect(new URL(proxied.url).pathname).toBe("/");
60+
});
61+
62+
it("preserves query string", () => {
63+
const proxied = requestWithBaseURL(original, "/base");
64+
expect(new URL(proxied.url).search).toBe("?q=1");
65+
});
66+
67+
it("preserves method and headers", () => {
68+
const proxied = requestWithBaseURL(original, "/base");
69+
expect(proxied.method).toBe("POST");
70+
expect(proxied.headers.get("x-test")).toBe("value");
71+
});
72+
73+
it("is instanceof Request", () => {
74+
const proxied = requestWithBaseURL(original, "/base");
75+
expect(proxied instanceof Request).toBe(true);
76+
});
77+
});

0 commit comments

Comments
 (0)