Public: anyone can read this; the crew can change it.

A HEAD of the front door is a 404 #193

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/180-a-head-of-the-front-door-is-a-404 → main 1 commit fork

  • Merged. scout/180-a-head-of-the-front-door-is-a-404 was merged into main as afb1b41.

Review

Sign in to review this change.
2 files changed +73 −1
src/http/server.ts +19 −1
@@ -1159,16 +1159,34 @@ export async function serve(app: App): Promise<Servers> {
11591159 * matched or the two shapes of "no route", the limiter, and the handler.
11601160 * The response it returns has not been given the security floor yet — that
11611161 * is the caller's single door, above.
1162 *
1163 * HEAD is answered here too, in one place: it routes as the GET it stands
1164 * in for — so a path with only a POST route still 404s — and the body is
1165 * dropped from whatever GET would have returned, after the one render that
1166 * produced it.
11621167 */
11631168 async function answer(
11641169 app: App,
11651170 router: Router<Handler>,
11661171 limiter: RateLimiter,
11671172 request: Request,
1173 ): Promise<Response> {
1174 const response = await answerGet(app, router, limiter, request);
1175 return request.method === "HEAD"
1176 ? new Response(null, { status: response.status, headers: response.headers })
1177 : response;
1178 }
1179
1180 async function answerGet(
1181 app: App,
1182 router: Router<Handler>,
1183 limiter: RateLimiter,
1184 request: Request,
11681185 ): Promise<Response> {
11691186 const url = publicUrl(request, app.config.url);
11701187 if (url.pathname === "/healthz") return healthResponse();
1171 const matched = router.match(request.method, url.pathname);
1188 const routeMethod = request.method === "HEAD" ? "GET" : request.method;
1189 const matched = router.match(routeMethod, url.pathname);
11721190 if (!matched) {
11731191 // A path under no route at all is what a mistyped or stale studio
11741192 // URL lands on. A browser gets the forge's own page for it, same as
test/web.test.ts +54
@@ -645,6 +645,60 @@ describe("readiness", () => {
645645 });
646646 });
647647
648 describe("HEAD mirrors the GET it stands in for", () => {
649 let dataDir: string;
650 let app: App;
651 let servers: Servers;
652 let base: string;
653
654 beforeAll(async () => {
655 dataDir = await mkdtemp(join(tmpdir(), "forge-head-"));
656 app = createApp({ dataDir, port: 0 });
657 servers = await serve(app);
658 base = servers.url;
659 });
660
661 afterAll(async () => {
662 servers.stop();
663 app.close();
664 await rm(dataDir, { recursive: true, force: true });
665 });
666
667 test("HEAD / is 200, empty, with the same content-type and security floor as GET /", async () => {
668 const get = await fetch(`${base}/`);
669 const getBody = await get.text();
670 expect(getBody.length).toBeGreaterThan(0);
671
672 const head = await fetch(`${base}/`, { method: "HEAD" });
673 expect(head.status).toBe(get.status);
674 expect(await head.text()).toBe("");
675 expect(head.headers.get("content-type")).toBe(get.headers.get("content-type"));
676 expect(head.headers.get("x-content-type-options")).toBe("nosniff");
677 expect(head.headers.get("referrer-policy")).toBeTruthy();
678 expect(head.headers.get("content-security-policy")).toContain("nonce-");
679 });
680
681 test("HEAD /healthz is 200 with an empty body", async () => {
682 const head = await fetch(`${base}/healthz`, { method: "HEAD" });
683 expect(head.status).toBe(200);
684 expect(await head.text()).toBe("");
685 });
686
687 test("HEAD of a path with no route at all is 404", async () => {
688 const head = await fetch(`${base}/no/such/route`, { method: "HEAD" });
689 expect(head.status).toBe(404);
690 expect(await head.text()).toBe("");
691 });
692
693 test("HEAD of a path that only has a POST route is 404, and never runs the handler", async () => {
694 const head = await fetch(`${base}/theme`, { method: "HEAD" });
695 expect(head.status).toBe(404);
696 expect(await head.text()).toBe("");
697 // The POST handler would have set a cookie; a HEAD must never reach it.
698 expect(head.headers.get("set-cookie")).toBeNull();
699 });
700 });
701
648702 const FLOOR_POLICY =
649703 "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
650704

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs