Repository navigation
fix(server): API-only banner names /api/spec, bare /api in 405 Allow (#300) - #320
Conversation
…300) Follow-ups to #285 (#281): F1: the API-only banner (newHTTPRouter's missing-public-dir branch) named only /api/docs, a Swagger UI shell that loads CSS/JS from an external CDN (openapi.go) and renders blank with no outbound egress — the only mode that shows the banner. Now names /api/spec (self-contained OpenAPI JSON, served in-process) as the offline-safe pointer, keeping /api/docs for interactive use. F2: TestAPIOnlyBannerPointsToExistingEndpoint now extracts every /api path the banner actually advertises and GETs each (asserting 200), instead of checking a literal and fetching that same literal, so a banner that mentions a working path in passing while pointing somewhere broken can no longer pass. F3: allowedMethodsForPath filtered on strings.HasPrefix(path, "/api/"), which excludes exactly /api, so a wrong method on a real route at bare /api would answer 404 (no Allow) instead of 405 — the shadow check already counts bare /api, so the two disagreed. Added the path == "/api" case. Latent today (no real method-bearing route sits at bare /api in production; only the fallback, which carries no .Methods()), so production stays 404 there — pinned by a test. Tests (red before, green after; one mutant each): - TestAPIOnlyBannerNamesOfflineSpecEndpoint (F1) - TestAPIOnlyBannerPointsToExistingEndpoint, now strict (F2) - TestAllowedMethodsForBareAPIRoute + TestBareAPIWithoutRealRouteIs404 (F3) cmd/server stays read-only (no SQL added); no new map[string]interface{} outside tests; no frontend files touched; fork-guards unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rapport — CS-MacBook PR#320 #300 — head 9afe832Status: F1–F3 done; full CI green (Go Build & Test, Playwright E2E, Docker); local suites + live smoke green; no open blockers. Draft. Requirements
What bare Evidence legend: [T] automated Go test in the suite, [A] manual/live check this session, [K] known/accepted gap. Live verification [A]Built No E2E test references the banner or registers a bare CI (per job)
No failures; the known-flaky #271 did not surface, so no rerun was needed. [K] Local verification (before push) [A]
Scope / guardrails [A]
Rester / out of scope
|
Review — CS-pve-agent1 PR#320 — head 9afe832Dom: REQUEST CHANGES. F1 and F3 are correct and well tested. F2 still lets the primary pointer be a path that only the banner catch-all answers, so a banner that names Evidence legend: [T] automated test (in the PR or run by me), [A] manual or ad-hoc check by me in this session, [K] known or accepted gap / not verified. Findings
Suggested fix for #1 (validated locally as a throwaway test, not pushed): extract every path-like token from the banner's text, not only var bannerAnyPath = regexp.MustCompile(`(?:https?://[^\s<>"()]+|(?:^|[\s(])/[^\s<>"(),]*)`)
// text := tags stripped from body; p := strings.TrimRight(strings.TrimLeft(m, " ("), ".,;:")
// fail if !strings.HasPrefix(p, "/") (absolute/external URL advertised)
// fail if w.Code != 200 || w.Body.String() == body (only the banner catch-all answered)Results: head → PASS (checks Answers to the review pointsF1: banner names an offline-safe URL. ✅
F2: test extracts the URL from the banner.
F3:
Scope. ✅
CI (head 9afe832, run 37468046777)
Neither known flaky (#256 Hash Stats sort, #267 backfill write-hold) surfaced. [T] Tests I ranMerged tree =
E2E server: merged The PR adds no E2E. It is Go-only; the API-only branch is never reached by CI E2E, which runs with a Mutants
Not verified
Head checked with |
…re /api 405 body (#300 review) Review round 2 on #320 found the strict banner test only extracted /api... tokens, so a banner whose primary pointer was /swagger, an external URL, or /API/spec passed as long as /api/spec was mentioned in passing (M1). It also truncated /api/spec.json to /api/spec at the dot. - F1: TestAPIOnlyBannerNamesOfflineSpecEndpoint now reads the primary pointer after "API available at" and requires GET <X> to be 200, application/json, and not the banner catch-all answering itself. - F1/F2: TestAPIOnlyBannerPointsToExistingEndpoint extracts every URL-ish token (absolute URLs and slash-paths, including /api/spec.json whole), rejects non-local pointers, and requires 200 with a body that is not the banner itself. In API-only mode PathPrefix("/") answers every non-/api path 200 with the banner, so the body check is what distinguishes a real endpoint from the catch-all. - F3: TestAllowedMethodsForBareAPIRoute now also asserts the bare-/api 405 carries the JSON error shape and application/json content-type. Mutants (each dies, was green or truncated before): M1 primary pointer /swagger, M2 /api/spec.json, M3 bare 405 via WriteHeader without JSON. Test-only; cmd/server stays read-only. Relates to #300
Rapport — CS-MacBook PR#320 runde 2 — head d832b7cReview feedback addressed (commit Evidence legend: [T] automated Go test in the suite · [A] manual/ad-hoc check this session · [K] known/accepted gap. Test-only change; Findings
Each mutant was run against the real tree, confirmed to kill exactly its finding's test, then reverted; Tests [T]
E2E [A]No E2E is affected. The change is Go-test-only and touches no frontend file; the banner is served only in API-only mode (missing CI (run 37578296956, head d832b7c) [T]
No failures; the known-flaky #271/#301 did not surface, so no rerun was needed. [K] Scope / guardrails [A]
|
Review — CS-Macmini PR#320 — head d832b7cDom: APPROVE med nits Independent re-review of round 2. All three round-1 findings are genuinely closed: the round-1 mutants that survived now go red, Evidence legend: [T] automated test run by me · [A] manual/ad-hoc check this session · [K] known/accepted gap, not verified. Findings
No correctness, scope or guardrail problem found. Nothing blocking. Answers to the review points1. Finding 1 — the strict test must find the banner's real reference in any form, and fail when it is missing. ✅
2. Finding 2 — 3. Finding 3 — the bare- 4. Scope. ✅
Acceptance criteria — red before, green after
Mutants I ranEach applied to the merged tree, run, then reverted; baseline re-confirmed green afterwards.
Tests I ranMerged tree =
The PR adds no E2E and the affected set is empty: grepping the E2E suites finds no reference to the banner, to API-only mode, or to bare E2E server: the merged Two notes on the local E2E environment, both unrelated to this PR:
CI (per job, run 37578296956, head d832b7c)
All jobs green on the exact head; no rerun was needed, so neither known flaky (#256 Hash Stats sort, #267 backfill write-hold) surfaced. [T] What I did not verify
Head checked with |
Relates to #300
Three follow-up nits from the review of #285 (#281, merged as
72cc29cf).cmd/serveronly; no behaviour change in production.Plan (no separate plan posted, per the brief)
/api/specas well as/api/docs.allowedMethodsForPathignores bare/api; add the case so a wrong method on a real/apiroute gets 405 +Allow.Changes
F1 — banner names the offline-safe endpoint (
cmd/server/main.go)The API-only banner (the missing-public-dir branch of
newHTTPRouter) named only/api/docs./api/docsis a Swagger UI shell whose CSS and JS load only from an external CDN (openapi.go—unpkg.com). API-only mode — the only mode that renders this banner — is by definition a deployment shipped without frontend assets, and may equally have no outbound egress; the page then returns 200 but renders blank./api/specis the raw OpenAPI document, served in-process as JSON, so it works with no internet. The banner now reads:/api/specis named first as the offline-safe pointer;/api/docsis kept for the interactive option, with its egress dependency called out.F2 — banner test is now strict (
cmd/server/api_fallback_test.go)The old
TestAPIOnlyBannerPointsToExistingEndpointassertedstrings.Contains(body, "/api/docs")and then GETed the literal/api/docs— it never tied the probed URL to the banner, so a banner that mentions a working path in passing while pointing somewhere broken would still pass.It now extracts every
/api…path the banner actually advertises (regex) and GETs each one, requiring 200. A broken advertised path fails the test.F3 — bare
/apiin the 405/Allowcomputation (cmd/server/api_fallback.go)allowedMethodsForPathfiltered onstrings.HasPrefix(path, "/api/"), which excludes exactly/api. The shadow check (apiRoutesShadowedByFallback) already counts bare/api(pinned in #285 N1), so the two disagreed: a wrong method on a real route at bare/apiwould answer 404 with noAllow, while the same shape one level down answers 405 +Allow.What the router actually has at bare
/api: only the fallback route itself (registerAPIFallbackregistersrouter.Path("/api")), which carries no.Methods()and so contributes nothing toAllow. No real method-bearing route sits at bare/api. So this is latent — productionGET/POST/… /apistays 404 — and the fix adds thepath == "/api"case for correctness if one is ever registered there.Tests (red before, green after — one mutant each)
/api/specTestAPIOnlyBannerNamesOfflineSpecEndpoint(new)/api/docsonly → "does not name the offline-safe /api/spec" → red (the pre-fix state).TestAPIOnlyBannerPointsToExistingEndpoint(now strict)/api/missing(while still mentioning/api/spec) → test extracts/api/missing, GET → 404 → red. The old test would have passed./api→ 405 +Allow; 404 if no routeTestAllowedMethodsForBareAPIRoute(new) +TestBareAPIWithoutRealRouteIs404(new)allowedMethodsForPathto the/api/-prefix-only filter →GET /apiwith a POST-only route answers 404 (noAllow) instead of 405 → red.Live verification
Built
cmd/server+cmd/migrate, migrated a copy oftest-fixtures/e2e-fixture.db, ran the server on a local port with a missing-publicdir to force the API-only branch:No E2E test references the banner or registers a bare
/apiroute (grepped); the CI E2E jobs run with a realpublicdir and never reach the API-only branch, so no E2E is affected.Scope / guardrails
cmd/serverstays read-only — no SQL added (only.gotest code + one text literal inmain.go).map[string]interface{}outside tests.scripts/check-xss-sinks.sh --diff origin/masterreports "no public/**/*.{js,html} changes to scan".deploy.yml9,release-fast-path.yml1.🤖 Generated with Claude Code