Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/2.utils/1.request.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,16 @@ app.get("/", (event) => {
});
```

### `requestWithBaseURL(req, base)`

Create a lightweight request proxy with the base path stripped from the URL pathname.

### `requestWithURL(req, url)`

Create a lightweight request proxy that overrides only the URL.

Avoids cloning the original request (no `new Request()` allocation).

### `toRequest(input, options?)`

Convert input into a web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).
Expand Down
5 changes: 2 additions & 3 deletions src/h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createRouter, addRoute, findRoute } from "rou3";
import { H3Event } from "./event.ts";
import { toResponse, kNotFound } from "./response.ts";
import { callMiddleware, normalizeMiddleware } from "./middleware.ts";
import { requestWithBaseURL } from "./utils/request.ts";

import type { ServerRequest } from "srvx";
import type { H3Config, H3CoreConfig, H3Plugin, MatchedRoute, RouterContext } from "./types/h3.ts";
Expand Down Expand Up @@ -142,9 +143,7 @@ export const H3 = /* @__PURE__ */ (() => {
} else {
const fetchHandler = "fetch" in input ? input.fetch : input;
this.all(`${base}/**`, function _mountedMiddleware(event) {
const url = new URL(event.url);
url.pathname = url.pathname.slice(base.length) || "/";
return fetchHandler(new Request(url, event.req));
return fetchHandler(requestWithBaseURL(event.req, base));
});
}
return this;
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export { type RouteDefinition, defineRoute } from "./utils/route.ts";
// Request

export {
requestWithURL,
requestWithBaseURL,
toRequest,
getRequestHost,
getRequestIP,
Expand Down
30 changes: 30 additions & 0 deletions src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,36 @@ import type { HTTPMethod } from "../types/h3.ts";
import type { H3EventContext } from "../types/context.ts";
import type { ServerRequest } from "srvx";

const _urlOverrides = new WeakMap<Request, string>();

const _proxyHandler: ProxyHandler<Request> = {
get(target, prop, receiver) {
if (prop === "url") return _urlOverrides.get(receiver);
const value = Reflect.get(target, prop);
return typeof value === "function" ? value.bind(target) : value;
},
};

/**
* Create a lightweight request proxy that overrides only the URL.
*
* Avoids cloning the original request (no `new Request()` allocation).
*/
export function requestWithURL(req: ServerRequest, url: string): ServerRequest {
const proxy = new Proxy(req, _proxyHandler);
_urlOverrides.set(proxy, url);
return proxy;
}

/**
* Create a lightweight request proxy with the base path stripped from the URL pathname.
*/
export function requestWithBaseURL(req: ServerRequest, base: string): ServerRequest {
const url = new URL(req.url);
url.pathname = url.pathname.slice(base.length) || "/";
return requestWithURL(req, url.href);
}

/**
* Convert input into a web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).
*
Expand Down
2 changes: 2 additions & 0 deletions test/unit/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ describe("h3 package", () => {
"readValidatedBody",
"redirect",
"removeResponseHeader",
"requestWithBaseURL",
"requestWithURL",
"requireBasicAuth",
"sanitizeStatusCode",
"sanitizeStatusMessage",
Expand Down
77 changes: 77 additions & 0 deletions test/unit/request.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import { requestWithURL, requestWithBaseURL } from "../../src/utils/request.ts";

describe("requestWithURL", () => {
const original = new Request("http://example.com/base/path", {
method: "POST",
headers: { "x-test": "value" },
body: "hello",
});

it("overrides url", () => {
const proxied = requestWithURL(original, "http://example.com/path");
expect(proxied.url).toBe("http://example.com/path");
});

it("preserves original url on source request", () => {
requestWithURL(original, "http://example.com/path");
expect(original.url).toBe("http://example.com/base/path");
});

it("preserves method", () => {
const proxied = requestWithURL(original, "http://example.com/path");
expect(proxied.method).toBe("POST");
});

it("preserves headers", () => {
const proxied = requestWithURL(original, "http://example.com/path");
expect(proxied.headers.get("x-test")).toBe("value");
});

it("is instanceof Request", () => {
const proxied = requestWithURL(original, "http://example.com/path");
expect(proxied instanceof Request).toBe(true);
});

it("clone() works and keeps overridden url", () => {
const proxied = requestWithURL(original, "http://example.com/path");
const cloned = proxied.clone();
expect(cloned.url).toBe("http://example.com/base/path");
expect(cloned.method).toBe("POST");
});
});

describe("requestWithBaseURL", () => {
const original = new Request("http://example.com/base/path?q=1", {
method: "POST",
headers: { "x-test": "value" },
body: "hello",
});

it("strips base from pathname", () => {
const proxied = requestWithBaseURL(original, "/base");
expect(proxied.url).toBe("http://example.com/path?q=1");
});

it("returns / when pathname equals base", () => {
const req = new Request("http://example.com/base");
const proxied = requestWithBaseURL(req, "/base");
expect(new URL(proxied.url).pathname).toBe("/");
});

it("preserves query string", () => {
const proxied = requestWithBaseURL(original, "/base");
expect(new URL(proxied.url).search).toBe("?q=1");
});

it("preserves method and headers", () => {
const proxied = requestWithBaseURL(original, "/base");
expect(proxied.method).toBe("POST");
expect(proxied.headers.get("x-test")).toBe("value");
});

it("is instanceof Request", () => {
const proxied = requestWithBaseURL(original, "/base");
expect(proxied instanceof Request).toBe(true);
});
});
Loading