fix(web): add SPA index fallback for embedded frontend routes

Serve existing static assets as-is, keep /api/* and missing asset paths returning 404, and add tests for SPA fallback behavior on refresh.
This commit is contained in:
wenjie 2026-03-06 17:44:03 +08:00
parent 1159646345
commit 71cd03430a
2 changed files with 92 additions and 2 deletions

View file

@ -5,6 +5,8 @@ import (
"io/fs" "io/fs"
"log" "log"
"net/http" "net/http"
"path"
"strings"
) )
//go:embed all:dist //go:embed all:dist
@ -24,6 +26,44 @@ func registerEmbedRoutes(mux *http.ServeMux) {
return return
} }
// Serve the static files at the root route fileServer := http.FileServer(http.FS(subFS))
mux.Handle("/", http.FileServer(http.FS(subFS)))
// Serve static assets and fallback to index.html for SPA routes.
mux.Handle(
"/",
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.NotFound(w, r)
return
}
// Keep unknown API paths as 404 instead of falling back to SPA entry.
if r.URL.Path == "/api" || strings.HasPrefix(r.URL.Path, "/api/") {
http.NotFound(w, r)
return
}
cleanPath := path.Clean(strings.TrimPrefix(r.URL.Path, "/"))
if cleanPath == "." {
cleanPath = ""
}
// Existing static files/directories should be served directly.
if cleanPath != "" {
if _, statErr := fs.Stat(subFS, cleanPath); statErr == nil {
fileServer.ServeHTTP(w, r)
return
}
// Missing asset-like paths should remain 404.
if strings.Contains(path.Base(cleanPath), ".") {
fileServer.ServeHTTP(w, r)
return
}
}
indexReq := r.Clone(r.Context())
indexReq.URL.Path = "/"
fileServer.ServeHTTP(w, indexReq)
}),
)
} }

50
web/backend/embed_test.go Normal file
View file

@ -0,0 +1,50 @@
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestSPARouteFallsBackToIndex(t *testing.T) {
mux := http.NewServeMux()
registerEmbedRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/providers", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusOK)
}
if !strings.Contains(rr.Body.String(), `<div id="root"></div>`) {
t.Fatalf("response does not look like index.html")
}
}
func TestUnknownAPIPathStays404(t *testing.T) {
mux := http.NewServeMux()
registerEmbedRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/api/not-found", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusNotFound)
}
}
func TestMissingAssetStays404(t *testing.T) {
mux := http.NewServeMux()
registerEmbedRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/assets/not-found.js", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusNotFound)
}
}