go-embed-directory-prefix-and-lazy-fs-sub
Diagnosed, fixed, and verified. The deliverable is at ~/GAP-024.md.
Root cause — two stacked pitfalls:
go:embed all:dist (and all:dist/*) preserves the dist/ prefix. Verified empirically with a real embed.FS on Go 1.26: both directive forms produce a tree with a single top-level dist directory containing index.html; only files embedded directly land at the FS root. The serving code checked fs.Stat(web.Dist, "index.html") at the root, it failed, and the handler fell through to the API catch-all → {"error":"route not found: GET /"}. The UI was embedded — just unreachable at the wrong path.
fs.Sub is lazy in Go 1.26. Confirmed in /usr/lib/go-1.26/src/io/fs/sub.go: Sub returns &subFS{fsys, dir} after only a ValidPath check — no stat of dir. And embed.FS has no Sub method (doesn't implement fs.SubFS), so the lazy generic path is always taken. A naive if err := fs.Sub(f, "dist"); err != nil { fallback } never fires on a root-shaped FS — proven with a probe.
Fix: a stat-then-sub rootEmbeddedFS adapter — fs.Stat(fsys, "dist") first (deterministic, real lookup), then fs.Sub, else fall back to the FS as-is. Serve http.FS(web.Root()) from http.FileServer.
Verification (live binary from /tmp, per the task):
- Buggy binary: GET / → 404 {"error":"route not found: GET /"} — exact reported symptom
- Fixed binary: GET / → 200 text/html with the real index.html; /assets/app.js → 200 text/javascript; /api/health → 200 JSON
- go vet + unit test passing for both embed shapes (dist/... and root-shaped)
One environment note: the sandbox already had services bound to :8090/:8091 (with hivemind-style CORS/CSP headers), so the repro binary used <ip-address>:18273; the reproduction project lives in /tmp/gap024.
# Evidence - Problem class: go-embed-directory-prefix-and-lazy-fs-sub - Model: openrouter/deepseek/deepseek-v4-flash-0731 - Solved: 2026-08-19T01:14:30.442Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "When embedding a frontend dist dir with //go:embed all:dist (or all:dist/*), the embedded FS nests everything under a dist/ prefix \u2014 go:embed preserves the matched directory name in both forms; only files directly in the package dir land at the FS root. Serving code must fs.Sub(web.Dist, \"dist\") before checking for index.html. Second pitfall: fs.Sub is LAZY in Go 1.26 \u2014 it wraps in subFS without validating the dir exists, so an error-check fallback never fires on a root-shaped FS; stat the dir first. Symptom: binary serves JSON 404 at / with no UI even though dist was built (embedded index.html unreachable at the wrong path). Fix: stat-then-sub adapter (rootEmbeddedFS), test with a real /tmp run of the binary, not just unit tests with synthetic FS.", "environment": "go-embed-serve-web-dist", "language": "go", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "go-embed-directory-prefix-and-lazy-fs-sub", "provider": "openrouter", "solved_at": "2026-08-19T01:14:30.442Z", "version": "1.26"}Diagnosed, fixed, and verified. The deliverable is at ~/GAP-024.md.
Root cause — two stacked pitfalls:
go:embed all:dist (and all:dist/*) preserves the dist/ prefix. Verified empirically with a real embed.FS on Go 1.26: both directive forms produce a tree with a single top-level dist directory containing index.html; only files embedded directly land at the FS root. The serving code checked fs.Stat(web.Dist, "index.html") at the root, it failed, and the handler fell through to the API catch-all → {"error":"route not found: GET /"}. The UI was embedded — just unreachable at the wrong path.
fs.Sub is lazy in Go 1.26. Confirmed in /usr/lib/go-1.26/src/io/fs/sub.go: Sub returns &subFS{fsys, dir} after only a ValidPath check — no stat of dir. And embed.FS has no Sub method (doesn't implement fs.SubFS), so the lazy generic path is always taken. A naive if err := fs.Sub(f, "dist"); err != nil { fallback } never fires on a root-shaped FS — proven with a probe.
Fix: a stat-then-sub rootEmbeddedFS adapter — fs.Stat(fsys, "dist") first (deterministic, real lookup), then fs.Sub, else fall back to the FS as-is. Serve http.FS(web.Root()) from http.FileServer.
Verification (live binary from /tmp, per the task):
- Buggy binary: GET / → 404 {"error":"route not found: GET /"} — exact reported symptom
- Fixed binary: GET / → 200 text/html with the real index.html; /assets/app.js → 200 text/javascript; /api/health → 200 JSON
- go vet + unit test passing for both embed shapes (dist/... and root-shaped)
One environment note: the sandbox already had services bound to :8090/:8091 (with hivemind-style CORS/CSP headers), so the repro binary used <ip-address>:18273; the reproduction project lives in /tmp/gap024.
# Evidence - Problem class: go-embed-directory-prefix-and-lazy-fs-sub - Model: openrouter/deepseek/deepseek-v4-flash-0731 - Solved: 2026-08-19T01:14:30.442Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "When embedding a frontend dist dir with //go:embed all:dist (or all:dist/*), the embedded FS nests everything under a dist/ prefix \u2014 go:embed preserves the matched directory name in both forms; only files directly in the package dir land at the FS root. Serving code must fs.Sub(web.Dist, \"dist\") before checking for index.html. Second pitfall: fs.Sub is LAZY in Go 1.26 \u2014 it wraps in subFS without validating the dir exists, so an error-check fallback never fires on a root-shaped FS; stat the dir first. Symptom: binary serves JSON 404 at / with no UI even though dist was built (embedded index.html unreachable at the wrong path). Fix: stat-then-sub adapter (rootEmbeddedFS), test with a real /tmp run of the binary, not just unit tests with synthetic FS.", "environment": "go-embed-serve-web-dist", "language": "go", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "go-embed-directory-prefix-and-lazy-fs-sub", "provider": "openrouter", "solved_at": "2026-08-19T01:14:30.442Z", "version": "1.26"}