fix(server): return 404 for missing content-hashed assets

A /assets/ chunk from a previous build no longer exists after a deploy;
serving the SPA shell at that URL makes the browser fail dynamic imports
on a text/html module. Exclude /assets/ from the SPA fallback so the
miss surfaces as a 404 the client can react to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rxwatcher
2026-07-22 13:43:57 +02:00
co-authored by Claude Fable 5
parent 845d969af1
commit bcc78e7332
2 changed files with 34 additions and 0 deletions
+9
View File
@@ -156,6 +156,15 @@ func (h *frontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
// A missing /assets/ file is a content-hashed chunk from another build,
// not an app route: answering with the shell makes dynamic imports fail
// on a text/html module. A 404 surfaces the real condition so the client
// preload-error handler can reload onto the current build.
if strings.HasPrefix(path, "/assets/") {
http.NotFound(w, r)
return
}
// SPA fallback: serve the (branded) index.html shell.
shell, ok := h.brandedShell(r)
if !ok {
+25
View File
@@ -79,6 +79,31 @@ func TestFrontendHandlerServesStaticAssetsWithoutCSP(t *testing.T) {
}
}
func TestFrontendHandlerReturns404ForMissingAssets(t *testing.T) {
handler := newFrontendTestHandler(t)
// A content-hashed chunk from a previous build no longer exists after a
// deploy. Serving the SPA shell at a .js URL makes the browser fail with
// "Failed to fetch dynamically imported module"; a 404 lets clients (and
// the preload-error reload handler) see the real condition.
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/assets/view-OldHash.js", nil))
if rr.Code != http.StatusNotFound {
t.Fatalf("missing asset status = %d, want 404", rr.Code)
}
if ct := rr.Header().Get("Content-Type"); strings.Contains(ct, "text/html") {
t.Fatalf("missing asset served as HTML (%q), the SPA fallback must not swallow /assets/", ct)
}
// Non-asset app routes still get the shell.
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/library/ebooks", nil))
if rr.Code != http.StatusOK || !strings.Contains(rr.Header().Get("Content-Type"), "text/html") {
t.Fatalf("SPA route = %d %q, want 200 HTML", rr.Code, rr.Header().Get("Content-Type"))
}
}
func newFrontendTestHandler(t *testing.T) http.Handler {
t.Helper()
prev := WebDistFS