mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-11 03:13:45 +00:00
fix(wsapi): set cache headers on the served frontend
Assets under _app/immutable carry a content hash in the name, so a changed file is a changed URL and the old one can be cached forever. The shell cannot: it names those hashed assets, so a copy cached across a deploy points at files that no longer exist and the app loads into a blank page with nothing in the log.
This commit is contained in:
1 parent
64106735ec
commit
e5b03d041d
2 files changed
+60
No files matched your search
@@ -2,6 +2,9 @@ package wsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -376,3 +379,48 @@ func TestRoomCodesAreUnbiased(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStaticCacheHeaders pins the two cache policies the single-page bundle
|
||||
// depends on. The failure they prevent is silent and total: an index.html
|
||||
// cached across a deploy names hashed assets that no longer exist, so the app
|
||||
// loads into a blank page with nothing in the log.
|
||||
func TestStaticCacheHeaders(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
assets := filepath.Join(dir, "_app", "immutable", "chunks")
|
||||
if err := os.MkdirAll(assets, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("<!doctype html>"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(assets, "abc123.js"), []byte("export{}"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
srv := NewServer(context.Background(), newTestDict("ngữ pháp", "pháp luật"), Config{WebDir: dir})
|
||||
defer srv.Shutdown()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"hashed asset", "/_app/immutable/chunks/abc123.js", "public, max-age=31536000, immutable"},
|
||||
{"the shell", "/", "no-cache"},
|
||||
{"a deep link falling back to the shell", "/play?difficulty=2", "no-cache"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, tc.path, nil))
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
if got := rec.Header().Get("Cache-Control"); got != tc.want {
|
||||
t.Errorf("Cache-Control = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,9 @@ func (s *Server) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||
s.hub.expireToken(sess.resumeToken, s.cfg.GraceFor)
|
||||
}
|
||||
|
||||
// immutablePrefix is where SvelteKit's adapter puts content-hashed assets.
|
||||
const immutablePrefix = "/_app/immutable/"
|
||||
|
||||
// mountStatic serves the built frontend so one binary is the whole deployment.
|
||||
//
|
||||
// Unknown paths fall back to index.html because the frontend is a single-page
|
||||
@@ -116,9 +119,18 @@ func (s *Server) mountStatic() {
|
||||
return
|
||||
}
|
||||
if info, err := os.Stat(clean); err == nil && !info.IsDir() {
|
||||
// Everything under immutablePrefix carries a content hash in its
|
||||
// name, so a changed file is a changed URL and the old one can be
|
||||
// cached forever.
|
||||
if strings.HasPrefix(r.URL.Path, immutablePrefix) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
}
|
||||
files.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// The shell names those hashed assets, so a cached copy outlives the
|
||||
// deploy that renamed them and the app loads into a blank page.
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
http.ServeFile(w, r, index)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user