// Package web embeds the compiled SPA assets (produced by `just web-build`) // into the Go binary via go:embed so the server binary is self-contained. // At build time `dist/` must be non-empty; the committed placeholder // index.html satisfies this on a fresh clone. The HTTP handler applies an // SPA fallback shim: any path that does not match a real file in the // embedded tree is rewritten to index.html so the client-side router can // handle it. Real static assets (hashed JS/CSS chunks) are served normally // with their correct Content-Type; the fallback only fires on 404s. package web import ( "embed" "fmt" "io/fs" "net/http" ) //go:embed all:dist var distFS embed.FS // Handler returns an http.Handler that serves the embedded SPA. Paths that // exist in the embedded tree are served directly. Paths that do not exist // fall back to index.html with HTTP 200 so the client-side router handles // them. If index.html itself cannot be opened from the embedded FS, the // handler returns 500 with a plain-text error. // // Routes beginning with /api/, /healthz, /readyz, or /metrics are NOT // this handler's concern; they must be mounted before this handler in the // router so they shadow the catch-all. func Handler() http.Handler { sub, err := fs.Sub(distFS, "dist") if err != nil { // This should never happen for a hard-coded path; blow up loudly. panic(fmt.Sprintf("web: fs.Sub on embedded dist: %v", err)) } fileServer := http.FileServer(http.FS(sub)) return &spaHandler{fs: sub, fileServer: fileServer} } // spaHandler wraps a standard file server with a SPA fallback: if the // requested path does not exist in the embedded FS, it serves index.html // instead of a 404. type spaHandler struct { fs fs.FS fileServer http.Handler } func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Strip the leading "/" to obtain the relative path within the FS. path := r.URL.Path if len(path) > 0 && path[0] == '/' { path = path[1:] } if path == "" { path = "index.html" } // Try to open the file to see if it exists. f, err := h.fs.Open(path) if err != nil { // File not found — apply SPA fallback: serve index.html at status 200. h.serveIndex(w, r) return } // Stat to confirm it's a regular file (not a directory that would // trigger a redirect to path+"/"). info, statErr := f.Stat() _ = f.Close() if statErr != nil || info.IsDir() { h.serveIndex(w, r) return } // File exists — let the standard file server handle it with correct // Content-Type, ETag, and range support. h.fileServer.ServeHTTP(w, r) } // serveIndex serves the embedded index.html with HTTP 200. It returns 500 // (plain-text) if the embedded FS cannot open the file — this is a build // invariant violation and must not be silently swallowed. func (h *spaHandler) serveIndex(w http.ResponseWriter, r *http.Request) { // Rewrite the URL to "/" so the file server finds index.html and sets // the correct Content-Type header. r2 := r.Clone(r.Context()) r2.URL.Path = "/" // Verify index.html is present before delegating; a missing file // produces a 500 rather than a misleading file-server 404. if _, err := h.fs.Open("index.html"); err != nil { http.Error(w, "internal server error: embedded index.html missing", http.StatusInternalServerError) return } h.fileServer.ServeHTTP(w, r2) }