Public: anyone can read this; the crew can change it.
Why did /explore answer 200 with an empty body once during release 0.7.374 proof? #9353
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9343-why-did-explore-answer-200-with-an-empty → main 3 commits fork 1 thread · 1 unresolved
2 files
changed +31
−2
src/release/smoke.ts +15 −1
| @@ -49,6 +49,20 @@ export function forgeMs(res: Response): number | undefined { | ||
| 49 | 49 | return app && Number.isFinite(app.ms) ? app.ms : undefined; |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | /** | |
| 53 | * What an empty 200 says about which layer lost the body (#9343): the forge | |
| 54 | * writes `Server-Timing` and `X-Forge-Version` and never an empty page, so | |
| 55 | * their absence points at the edge or the wire; a declared length with no | |
| 56 | * bytes points at a body cut short. | |
| 57 | */ | |
| 58 | export function emptyEvidence(res: Response): string { | |
| 59 | const seen = ["content-length", "content-encoding", "cf-cache-status", "age", "server-timing", "x-forge-version"] | |
| 60 | .map((name) => [name, res.headers.get(name)] as const) | |
| 61 | .filter((pair): pair is readonly [string, string] => pair[1] !== null) | |
| 62 | .map(([name, value]) => `${name}: ${value.slice(0, 80)}`); | |
| 63 | return seen.length === 0 ? " (no forge or edge headers)" : ` (${seen.join("; ")})`; | |
| 64 | } | |
| 65 | ||
| 52 | 66 | export interface Heavy { |
| 53 | 67 | watchMs?: number; |
| 54 | 68 | pollMs?: number; |
| @@ -180,7 +194,7 @@ async function watch(root: URL, token: string, heavy: Heavy, fetch: typeof globa | ||
| 180 | 194 | return gone(`/healthz reported X-Forge-Version ${res.headers.get("x-forge-version") ?? "none"}, wanted ${heavy.version}`, res.status); |
| 181 | 195 | if (path === "/boards" && anonymousBoard && !token && isSignInRedirect(url, res)) return { failure: null, slow: false }; |
| 182 | 196 | if (res.status !== 200) return gone(`${path} expected 200`, res.status); |
| 183 | if (!body.trim()) return { ...gone(`${path} answered with an empty body`, res.status), empty: true }; | |
| 197 | if (!body.trim()) return { ...gone(`${path} answered with an empty body${emptyEvidence(res)}`, res.status), empty: true }; | |
| 184 | 198 | return { failure: null, slow: false }; |
| 185 | 199 | } catch (error) { |
| 186 | 200 | return { ...gone(`${path} did not answer within ${slowMs * 2} ms (${error})`), hung: true }; |
test/release-smoke.test.ts +16 −1
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { afterEach, expect, test } from "bun:test"; |
| 2 | import { HEAVY_PAGES, forgeMs, smoke } from "../src/release/smoke.ts"; | |
| 2 | import { HEAVY_PAGES, emptyEvidence, forgeMs, smoke } from "../src/release/smoke.ts"; | |
| 3 | 3 | |
| 4 | 4 | let server: ReturnType<typeof Bun.serve> | undefined; |
| 5 | 5 | afterEach(() => server?.stop(true)); |
| @@ -153,6 +153,15 @@ test("a /healthz that gets no answer twice in a row in the watch fails (#7724)", | ||
| 153 | 153 | expect(failures.some((f) => f.url.endsWith("/healthz") && f.why.includes("did not answer"))).toBe(true); |
| 154 | 154 | }); |
| 155 | 155 | |
| 156 | test("an empty 200 names the headers that say which layer lost the body (#9343)", async () => { | |
| 157 | const empty = (p: string) => (p === "/explore" ? new Response("", { headers: { "cf-cache-status": "HIT", "server-timing": "app;dur=3" } }) : undefined); | |
| 158 | const failures = await smoke(forge(0, [], empty), "t", { heavy }); | |
| 159 | const f = failures.find((x) => x.url.endsWith("/explore")); | |
| 160 | expect(f?.why).toContain("answered with an empty body"); | |
| 161 | expect(f?.why).toContain("cf-cache-status: HIT"); | |
| 162 | expect(f?.why).toContain("server-timing: app;dur=3"); | |
| 163 | }); | |
| 164 | ||
| 156 | 165 | test("a heavy page that answers an empty body once is asked again and passes (#9334)", async () => { |
| 157 | 166 | let n = 0; |
| 158 | 167 | const flaky = (p: string) => (p === "/explore" && ++n === 1 ? new Response("", { status: 200 }) : undefined); |
| @@ -180,3 +189,9 @@ test("a polled heavy page that answers an empty body twice in a row in the watch | ||
| 180 | 189 | const failures = await smoke(forge(0, [], flaky), "t", { heavy }); |
| 181 | 190 | expect(failures.some((f) => f.url.endsWith("/explore") && f.why.includes("empty body"))).toBe(true); |
| 182 | 191 | }); |
| 192 | ||
| 193 | test("an empty 200 with no forge or edge headers says so, pointing past the forge (#9343)", () => { | |
| 194 | // Called directly: a real server adds a content-length of its own to any body. | |
| 195 | expect(emptyEvidence(new Response(""))).toBe(" (no forge or edge headers)"); | |
| 196 | expect(emptyEvidence(new Response("", { headers: { "x-forge-version": "v1" } }))).toBe(" (x-forge-version: v1)"); | |
| 197 | }); |
new above
offline
The empty-evidence branch (no headers seen) is untested; delete it and every test still passes. Add a test for it next to the cf-cache-status one.