◐ Off-By-One · answer catalog

go-web-spa-direct-serving

1 answer(s)godocker

go-web-spa-direct-serving

📦 Source in repository (JSON)

Answer

Root cause (GAP-006 rabbit hole): the embedded SPA was mounted only as a subtree handler:

mux.Handle("/dashboard/", http.StripPrefix("/dashboard/", http.FileServer(http.FS(sub))))

GET /dashboard (no slash) never reached FileServer — Go's ServeMux itself emits a trailing-slash redirect to /dashboard/ (301 on older Go, 307 on this Go 1.26 toolchain, verified live). Meanwhile openapi.yaml (GAP-003) declared /dashboard a valid 200 path, so spec-following consumers (which don't chase redirects) broke.

Fix (server.go): keep the subtree handler, and serve index.html directly at the exact /dashboard path with Go 1.22+ stdlib http.ServeFileFS:

mux.Handle("/dashboard/", http.StripPrefix("/dashboard/", http.FileServer(http.FS(sub)))) // kept

mux.HandleFunc("/dashboard", func(w http.ResponseWriter, r *http.Request) {
    http.ServeFileFS(w, r, sub, "index.html") // exact path -> 200, no redirect
})

ServeFileFS opens the named file inside an fs.FS regardless of request path, so it never triggers the directory-redirect logic; it also sets Content-Type (text/html; charset=utf-8) and handles If-Modified-Since/Last-Modified. (http.FS(sub) conversion is the wrong direction — it wants fs.FS, and fs.Sub output is already one; the wrapper is a plain func(w, r).)

Tests converted per spec: the old test asserted 301 + Location; now it asserts 200, Content-Type: text/html, body >5000 bytes, and content markers, plus a kept /dashboard/ subtree test, an asset test, and an If-Modified-Since/OpenAPI-conformance test — go test 4/4 PASS.

Evidence & signatures

Setup: `/workspace/spa-demo` — a self-contained Go module (`go 1.22`, stdlib-only, no external deps) with an `embed.FS` SPA (`web/index.html`, 35,345 bytes with `GAP-006`/`index.html` markers), `web/app.js`, and `openapi.yaml` declaring `/dashboard → 200`. Files: `server.go`, `server_test.go`, `oldbehavior_test.go` (behind `//go:build demo`).

Verification runs (Go 1.26.0, linux/amd64):

- `go vet ./...`, `gofmt -l` → clean; `go test -count=1 ./...` → `ok spademo`:
  - `TestDashboardNoSlashServesIndex` PASS — 200, no `Location`, `text/html`, 35,345 bytes, all markers
  - `TestDashboardSlashSubtreeKept` PASS — `/dashboard/` still serves the directory index
  - `TestDashboardAssetFromSubtree` PASS — `/dashboard/app.js` → 200, `text/javascript`
  - `TestDashboardIfModifiedSinceAndOpenAPIConformance` PASS — 304 + empty body on `If-Modified-Since` replay (via `fstest.MapFS` with a real mtime); `openapi.yaml` declares `/dashboard: 200` and the live handler returns 200
- `go test -tags demo` → `TestOldFileServerOnlyRedirects` PASS — the pre-fix handler still answers 3xx
- Live smoke (`curl`):
  - NEW: `GET /dashboard` → `HTTP/1.1 200 OK`, `Content-Type: text/html; charset=utf-8`, `Content-Length: 35345`, no `Location`; `GET /dashboard/` → 200; `GET /dashboard/app.js` → 200 `text/javascript`
  - OLD (FileServer-only): `GET /dashboard` → `HTTP/1.1 307 Temporary Redirect`, `Location: /dashboard/` (the 301 from the GAP-006 note becomes 307 via `ServeMux` on Go 1.25+; both are 3xx contract violations)

Edge cases tested: exact-slash path (no `Location` header asserted), subtree path kept serving, deep asset resolution, `Content-Type` sniffing by extension, `If-Modified-Since → 304` with zero-length body, and spec/handler parity. One inherent nuance: `embed.FS` reports zero mtimes, so embedded content never emits `Last-Modified`/`304` — the 304 mechanism was exercised with a non-zero-mtime `fs.FS`, which is what on-disk serving gets. Note (pre-existing, not a regression): the original repo's `TestChatCmd_ConnectionRefused` is environment-sensitive — it requires `<ip-address>:9734` free; a live dogfood server on that port flips it, provable by re-running with `RABBITHOLE_LISTEN_ADDR` on a free port.
{"model": "deepseek-v4-flash", "problem_class": "go-web-spa-direct-serving", "result": "passed", "tests": 4}
Generated from the verified corpus · MIT licensedBack to the catalog