Skip to content
Open
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
14 changes: 12 additions & 2 deletions src/dev/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { H3Event, HTTPHandler } from "h3";
import { createProxyServer, type ProxyServerOptions } from "httpxy";
import type { IncomingMessage, ServerResponse } from "node:http";
import { H3, toEventHandler, serveStatic, fromNodeHandler, HTTPError } from "h3";
import { joinURL } from "ufo";
import { joinURL, withoutTrailingSlash } from "ufo";
import mime from "mime";
import { join, resolve, extname } from "pathe";
import { stat } from "node:fs/promises";
Expand Down Expand Up @@ -96,7 +96,17 @@ export class NitroDevApp {
opts = { target: opts };
}
const proxy = createHTTPProxy(opts);
app.all(route, proxy.handleEvent);
const prefix = withoutTrailingSlash(route.replace(/\/?\*+$/, ""));
const base = prefix === "/" ? "" : prefix;
app.all(joinURL(prefix, "**"), async (event) => {
const originalPathname = event.url.pathname;
event.url.pathname = originalPathname.slice(base.length) || "/";
try {
return await proxy.handleEvent(event);
} finally {
event.url.pathname = originalPathname;
}
});
}

// Main handler
Expand Down
90 changes: 90 additions & 0 deletions test/unit/dev-proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createServer, type Server } from "node:http";
import type { AddressInfo } from "node:net";
import { serve, type ServerHandler } from "srvx";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { Nitro } from "nitro/types";
import { NitroDevApp } from "../../src/dev/app.ts";

describe("dev proxy", () => {
let target: Server;
let devServer: ReturnType<typeof serve>;
let devURL: string;

beforeAll(async () => {
target = createServer((req, res) => {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ url: req.url }));
});
await new Promise<void>((resolve) => target.listen(0, "127.0.0.1", () => resolve()));
const targetPort = (target.address() as AddressInfo).port;

const nitro = {
options: {
baseURL: "/",
devHandlers: [],
publicAssets: [],
devProxy: {
"/proxy": { target: `http://127.0.0.1:${targetPort}` },
"/wildcard/**": { target: `http://127.0.0.1:${targetPort}` },
"/slash/": { target: `http://127.0.0.1:${targetPort}` },
},
},
logger: console,
vfs: {},
} as unknown as Nitro;

const devApp = new NitroDevApp(nitro, () => "app");
devServer = serve({
port: 0,
hostname: "127.0.0.1",
fetch: devApp.fetch as ServerHandler,
});
await devServer.ready();
devURL = devServer.url!;
});

afterAll(async () => {
await devServer?.close(true);
await new Promise<void>((resolve) => target.close(() => resolve()));
});

const req = async (path: string) => {
const res = await fetch(new URL(path, devURL));
return { status: res.status, body: await res.text() };
};

it("should proxy the exact route", async () => {
expect(await req("/proxy")).toMatchObject({ status: 200, body: '{"url":"/"}' });
});

it("should proxy the trailing slash form", async () => {
expect(await req("/proxy/")).toMatchObject({ status: 200, body: '{"url":"/"}' });
});

it("should proxy nested subpaths with a query string", async () => {
expect(await req("/proxy/foo/bar?x=1")).toMatchObject({
status: 200,
body: '{"url":"/foo/bar?x=1"}',
});
});

it("should not proxy sibling routes with the same prefix", async () => {
expect(await req("/proxyable")).toMatchObject({ status: 200, body: "app" });
});

it("should treat a wildcard rule the same as a bare prefix", async () => {
expect(await req("/wildcard")).toMatchObject({ status: 200, body: '{"url":"/"}' });
expect(await req("/wildcard/foo?x=1")).toMatchObject({
status: 200,
body: '{"url":"/foo?x=1"}',
});
});

it("should treat a trailing slash rule the same as a bare prefix", async () => {
expect(await req("/slash")).toMatchObject({ status: 200, body: '{"url":"/"}' });
expect(await req("/slash/foo?x=1")).toMatchObject({
status: 200,
body: '{"url":"/foo?x=1"}',
});
});
});
Loading