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
8 changes: 7 additions & 1 deletion packages/void/src/router/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,9 @@ function compilePageRoutes(lines: string[], scan: ScanResult, pages: PagesCompil

// Asset fallback — delegates to env.ASSETS binding (static files from dist/client)
lines.push(`async function fetchPagesAsset(c) {`);
lines.push(` if (c.req.method !== "GET" && c.req.method !== "HEAD") {`);
lines.push(` return new Response("Not Found", { status: 404 });`);
lines.push(` }`);
lines.push(` const assets = c.env?.ASSETS;`);
lines.push(` if (!assets || typeof assets.fetch !== "function") {`);
lines.push(` return new Response("Not Found", { status: 404 });`);
Expand Down Expand Up @@ -1075,6 +1078,9 @@ function compilePageRoutes(lines: string[], scan: ScanResult, pages: PagesCompil
lines.push(` if (path === "/api" || path.startsWith("/api/")) {`);
lines.push(` return c.text("Not Found", 404);`);
lines.push(` }`);
lines.push(` if (c.req.method !== "GET" && c.req.method !== "HEAD") {`);
lines.push(` return c.text("Not Found", 404);`);
lines.push(` }`);
// Try static assets for any path (not just dot-containing last segments)
lines.push(` const assetRes = await fetchPagesAsset(c);`);
lines.push(` if (assetRes.status !== 404) return assetRes;`);
Expand Down Expand Up @@ -1125,7 +1131,7 @@ export function compileWorkerEntry(
// Safe for workers without ASSETS binding (env.ASSETS check short-circuits).
const assetHelper = `
async function serveWithAssets(request, env, response) {
if (response.status !== 404 || !env.ASSETS) return response;
if (response.status !== 404 || !env.ASSETS || (request.method !== "GET" && request.method !== "HEAD")) return response;
const asset = await env.ASSETS.fetch(request);
if (asset.status === 404) return response;
const res = new Response(asset.body, asset);
Expand Down
2 changes: 1 addition & 1 deletion packages/void/test/integration/node-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe("playground/node-app build + production", () => {
env: { ...process.env, PORT: String(PROD_PORT) },
});

await waitForServer(PROD_BASE, 10_000);
await waitForServer(PROD_BASE, 20_000);
}, 60_000);

afterAll(async () => {
Expand Down
32 changes: 26 additions & 6 deletions test/integration-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
import { createServer } from "node:net";
import { createServer, type Server } from "node:net";
import { once } from "node:events";
import type { spawn } from "node:child_process";

/**
* Returns an unused port by briefly binding to port 0 and reading the
* OS-assigned port.
* Ports held by getPort() until waitForServer() releases them.
* Prevents TOCTOU races when many integration tests run in parallel —
* without this, another test's getPort() can grab the same port
* during the gap between close and the child process's listen().
*/
const heldServers = new Map<number, Server>();

/**
* Returns an unused port by binding to port 0 and reading the
* OS-assigned port. The underlying socket stays open until
* waitForServer() is called with a URL on that port, preventing
* other parallel tests from stealing it.
*/
export async function getPort(): Promise<number> {
const server = createServer();
server.listen(0);
server.listen(0, "127.0.0.1");
await once(server, "listening");
const { port } = server.address() as import("node:net").AddressInfo;
server.close();
await once(server, "close");
server.unref(); // don't block process exit if beforeAll throws before waitForServer
heldServers.set(port, server);
return port;
Comment thread
Brooooooklyn marked this conversation as resolved.
}

export async function waitForServer(url: string, timeoutMs: number): Promise<void> {
// Release the held port so the child process can bind to it.
// This is called right after spawn(), minimizing the TOCTOU window.
const urlPort = Number(new URL(url).port);
const held = heldServers.get(urlPort);
if (held) {
held.close();
await once(held, "close");
heldServers.delete(urlPort);
}

const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
Expand Down
Loading