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
75 changes: 75 additions & 0 deletions src/proxy/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,81 @@ describe("Proxy Handler", () => {
}
});

it("returns 404 for a hosted environment root that names no project", async () => {
// staging.veryfront.com parses as an environment root: a veryfront domain
// with slug null. It used to be forwarded with x-project-slug: "", which
// the runtime answers 502 "Missing project context" — a config gap
// reported as an upstream failure. It is the same condition a custom
// domain answers 404 for.
const { server, port } = createMockServer((req: Request) => {
const { pathname } = new URL(req.url);
if (pathname === "/auth/token") return createTokenResponse();
return createNotFoundResponse();
});

try {
const handler = createHandler(port);

for (
const host of [
"staging.veryfront.com",
"preview.veryfront.com",
"production.veryfront.com",
"staging.veryfront.org",
]
) {
const ctx = await handler.processRequest(
new Request(`http://${host}/page`, { headers: { host } }),
);

assertEquals(ctx.projectSlug, undefined);
assertEquals(ctx.error?.status, 404);
assertEquals(ctx.error?.message, `No project configured for domain: ${host}`);
}

await handler.close();
} finally {
await server.shutdown();
}
});

it("keeps project-less local dev hosts reachable for the project chooser", async () => {
// Locally a project-less veryfront host is not a misconfiguration: it is
// how the chooser is reached. ProjectsHandler is enabled for exactly
// `isVeryfrontDomain && !projectSlug`, so these must keep forwarding
// rather than 404 like their hosted counterparts.
const { server, port } = createMockServer((req: Request) => {
const { pathname } = new URL(req.url);
if (pathname === "/auth/token") return createTokenResponse();
return createNotFoundResponse();
});

try {
const handler = createHandler(port);

for (
const host of [
"lvh.me",
"veryfront.me",
"veryfront.dev",
"preview.lvh.me",
"staging.lvh.me",
]
) {
const ctx = await handler.processRequest(
new Request(`http://${host}/`, { headers: { host } }),
);

assertEquals(ctx.error, undefined, `${host} must not be an error context`);
assertEquals(ctx.contentSourceId, "no-project");
}

await handler.close();
} finally {
await server.shutdown();
}
});

it("returns 404 error when custom domain not found", async () => {
const { server, port } = createMockServer((req: Request) => {
const { pathname } = new URL(req.url);
Expand Down
23 changes: 22 additions & 1 deletion src/proxy/handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { TokenManager, type TokenScope } from "./token-manager.ts";
import { type ParsedDomain, parseProjectDomain } from "#veryfront/server/utils/domain-parser.ts";
import {
isHostedVeryfrontDomain,
type ParsedDomain,
parseProjectDomain,
} from "#veryfront/server/utils/domain-parser.ts";
import type { TokenCache } from "./cache/types.ts";
import { computeContentSourceId } from "#veryfront/cache/keys.ts";
import { getEnv } from "#veryfront/platform/compat/process.ts";
Expand Down Expand Up @@ -815,6 +819,23 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
}, logger);

if (!projectSlug && parsedDomain.isVeryfrontDomain) {
Comment thread
kojiwakayama marked this conversation as resolved.
// A hosted environment root (staging.veryfront.com) names no project and
// nothing downstream can supply one, so forwarding only sends
// x-project-slug: "" and earns 502 "Missing project context" — a
// configuration gap reported as an upstream failure. A custom domain in
// that state already answers 404.
//
// Locally the same shape means something else: on lvh.me and friends a
// project-less host is how the project chooser is reached, so those keep
// forwarding. See ProjectsHandler, enabled for exactly this state.
if (isHostedVeryfrontDomain(host)) {
logger?.info("No project for hosted veryfront domain", { host });
return createProxyErrorContext(base, {
status: 404,
message: `No project configured for domain: ${host}`,
});
}

return {
token: undefined,
projectSlug: undefined,
Expand Down
12 changes: 12 additions & 0 deletions src/server/utils/domain-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,18 @@ export function parseProjectDomain(host: string): ParsedDomain {
return createParsedDomain(null, null, null, false, false);
}

/**
* Whether the host is a hosted veryfront domain (veryfront.com / veryfront.org)
* rather than one of the local development domains.
*
* The two differ in what a project-less host means. Locally it means "no project
* chosen yet" and the project chooser answers; hosted it means the domain names
* no project at all and nothing can answer.
*/
export function isHostedVeryfrontDomain(host: string): boolean {
return new RegExp(`^(?:.+\\.)?(${PROD_DOMAINS})$`, "i").test(stripPort(host));
}

/**
* Check if a domain is a valid veryfront domain (includes veryfront.me and lvh.me for local dev)
*/
Expand Down