The apps subsystem previously sent Cache-Control: public, max-age=300|3600, must-revalidate but no ETag. With must-revalidate and no validator, the browser cannot return 304 — it has to refetch the full body once max-age expires. For mdedit that's 920 KB on every reload after an hour. Add a content-addressed ETag (sha256 hex prefix, 32 chars) to: - apps/handler.go's serveBody + serveEmbedded (both paths now emit ETag + handle If-None-Match short-circuit to 304) - handler/directory.go's embedded:browse fallback (mirror behavior so the bare-directory landing serves the same way) Drop max-age to 0 with must-revalidate: every page load revalidates, but a matching ETag returns 304 with empty body. Steady-state cost of a reload drops from N KB to a few hundred bytes. When the binary is redeployed, the ETag changes (content hash) and the next request returns 200 with the new bytes. Tests in apps/handler_test.go cover both paths: - TestServer_Embedded_ConditionalGET: full GET, matching INM, stale INM - TestEmbeddedETag_Stable: same bytes → same ETag, different → different Live smoke (curl against zddc-server -root /tmp/empty): GET / → 200, ETag set, body = 80919 bytes (landing.html) GET / + INM:tag → 304 Not Modified, empty body
89 lines
2.5 KiB
Go
89 lines
2.5 KiB
Go
package apps
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
_ "embed"
|
||
"sync"
|
||
)
|
||
|
||
// Embedded fallback: the five tool HTMLs from the time the binary was
|
||
// built. Used as a last-resort served-bytes when (cache miss) AND
|
||
// (upstream unreachable) AND (no operator override) — see handler.go.
|
||
//
|
||
// The files are populated by the top-level build.sh, which copies the
|
||
// freshly-built dist/<tool>.html into ./embedded/ before `go build` runs.
|
||
// Empty placeholder files are checked in so the package compiles when no
|
||
// build has been run yet (CI bootstrap, fresh clone, etc.); at runtime an
|
||
// empty embedded body is treated as "no embedded fallback available."
|
||
|
||
//go:embed embedded/archive.html
|
||
var embeddedArchive []byte
|
||
|
||
//go:embed embedded/transmittal.html
|
||
var embeddedTransmittal []byte
|
||
|
||
//go:embed embedded/classifier.html
|
||
var embeddedClassifier []byte
|
||
|
||
//go:embed embedded/mdedit.html
|
||
var embeddedMdedit []byte
|
||
|
||
//go:embed embedded/index.html
|
||
var embeddedLanding []byte
|
||
|
||
//go:embed embedded/browse.html
|
||
var embeddedBrowse []byte
|
||
|
||
// EmbeddedBytes returns the embedded HTML for app, or nil if either app is
|
||
// not one of the canonical names or the embedded slot is empty (no build
|
||
// has populated it).
|
||
func EmbeddedBytes(app string) []byte {
|
||
var b []byte
|
||
switch app {
|
||
case "archive":
|
||
b = embeddedArchive
|
||
case "transmittal":
|
||
b = embeddedTransmittal
|
||
case "classifier":
|
||
b = embeddedClassifier
|
||
case "mdedit":
|
||
b = embeddedMdedit
|
||
case "landing":
|
||
b = embeddedLanding
|
||
case "browse":
|
||
b = embeddedBrowse
|
||
default:
|
||
return nil
|
||
}
|
||
if len(b) == 0 {
|
||
return nil
|
||
}
|
||
return b
|
||
}
|
||
|
||
// EmbeddedETag returns a strong ETag (sha256-hex prefix, 32 chars) for the
|
||
// app's embedded bytes. Computed lazily on first call per-app and memoized
|
||
// — the embedded slot is fixed for the binary's lifetime, so the ETag
|
||
// changes only when the binary is redeployed. Empty slot returns "".
|
||
//
|
||
// Used by apps.Server.serveEmbedded to issue conditional-GET-friendly
|
||
// responses: with this ETag + Cache-Control: max-age=0, must-revalidate,
|
||
// every page load revalidates and gets a 304 unless the binary has been
|
||
// updated. Saves re-transmitting 50–920 KB tool HTMLs on every reload.
|
||
func EmbeddedETag(app string) string {
|
||
if v, ok := etagCacheByApp.Load(app); ok {
|
||
return v.(string)
|
||
}
|
||
body := EmbeddedBytes(app)
|
||
if body == nil {
|
||
return ""
|
||
}
|
||
sum := sha256.Sum256(body)
|
||
etag := hex.EncodeToString(sum[:])[:32]
|
||
etagCacheByApp.Store(app, etag)
|
||
return etag
|
||
}
|
||
|
||
// etagCacheByApp memoizes EmbeddedETag results keyed by app name.
|
||
var etagCacheByApp sync.Map
|