busy: the URL's version is the files' digest; HEAD sends Content-Length (review)
Claude Fable 5.1 (approve): nothing tied the prefix's v1 to the bytes, so a future re-copy could forget to bump it and browsers would keep the old busy.js for a year at an immutable URL. The segment is now the digest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+18
−9
busy/busy.go+11 −3busy/busy_test.go+7 −6
CI passed — run details
CI log
=== modcache === all modules verified === vet === CGO_ENABLED=0 go vet ./... === fmt === === build === CGO_ENABLED=0 go build -buildvcs=false ./... === go-test === CGO_ENABLED=0 go test -count=1 ./... ok amadan.net/oficina/estilo.git/base 0.012s ok amadan.net/oficina/estilo.git/brand 0.010s ok amadan.net/oficina/estilo.git/busy 0.010s ok amadan.net/oficina/estilo.git/callui 0.019s ok amadan.net/oficina/estilo.git/chrome 0.222s ? amadan.net/oficina/estilo.git/cmd/oficina-notices [no test files] ok amadan.net/oficina/estilo.git/credits 0.009s ok amadan.net/oficina/estilo.git/credits/catalog 0.007s ok amadan.net/oficina/estilo.git/homeconnect 0.008s ok amadan.net/oficina/estilo.git/internal/lint 0.007s ? amadan.net/oficina/estilo.git/internal/tokencheck [no test files] ok amadan.net/oficina/estilo.git/menu 0.011s ok amadan.net/oficina/estilo.git/savestatus 0.090s ok amadan.net/oficina/estilo.git/sky 0.024s ok amadan.net/oficina/estilo.git/theme 0.009s
diff --git a/busy/busy.go b/busy/busy.go| index c1410f8..0ec2fc4 100644 |
| --- a/busy/busy.go |
| +++ b/busy/busy.go |
| @@ -21,8 +21,11 @@ |
| package busy |
| import ( |
| + "crypto/sha256" |
| _ "embed" |
| + "encoding/hex" |
| "net/http" |
| + "strconv" |
| "strings" |
| ) |
| @@ -44,9 +47,13 @@ func JS() []byte { return append([]byte(nil), busyJS...) } |
| // CSS is the spinner-in-place block. |
| func CSS() []byte { return append([]byte(nil), busyCSS...) } |
| -// Prefix is the URL path Handler serves under. The version moves whenever |
| -// either file's bytes do: both are cached for a year. |
| -const Prefix = "/static/estilo/busy/v1/" |
| +// Prefix is the URL path Handler serves under. Its version segment is the |
| +// two files' own digest, so new bytes are always a new URL: both are |
| +// cached for a year, immutably, and no one has to remember to bump it. |
| +var Prefix = func() string { |
| + sum := sha256.Sum256(append(append([]byte(nil), busyJS...), busyCSS...)) |
| + return "/static/estilo/busy/" + hex.EncodeToString(sum[:6]) + "/" |
| +}() |
| // Handler serves GET for Prefix + "busy.js" and Prefix + "busy.css", and |
| // nothing else. Public and sessionless: the sign-in form needs it too. |
| @@ -72,6 +79,7 @@ func Handler() http.Handler { |
| w.Header().Set("Content-Type", kind) |
| w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") |
| w.Header().Set("X-Content-Type-Options", "nosniff") |
| + w.Header().Set("Content-Length", strconv.Itoa(len(body))) |
| if r.Method == http.MethodHead { |
| return |
| } |
diff --git a/busy/busy_test.go b/busy/busy_test.go| index 4e750ca..461afe2 100644 |
| --- a/busy/busy_test.go |
| +++ b/busy/busy_test.go |
| @@ -55,12 +55,13 @@ func TestTheShimLeftTheRuleToBusyJS(t *testing.T) { |
| func TestHandlerServesOnlyItsTwoFiles(t *testing.T) { |
| h := Handler() |
| for path, want := range map[string]int{ |
| - Prefix + "busy.js": 200, |
| - Prefix + "busy.css": 200, |
| - Prefix + "rastrillo.js": 404, |
| - Prefix + "../busy.go": 404, |
| - Prefix: 404, |
| - "/static/busy.js": 404, |
| + Prefix + "busy.js": 200, |
| + Prefix + "busy.css": 200, |
| + Prefix + "rastrillo.js": 404, |
| + Prefix + "../busy.go": 404, |
| + Prefix: 404, |
| + "/static/busy.js": 404, |
| + "/static/estilo/busy/v1/busy.js": 404, // the version is the digest, not a counter |
| } { |
| rec := httptest.NewRecorder() |
| h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil)) |