// Package imghttp holds the HTTP caching contract shared by the subsonic, public, and jellyfin // image handlers, so they apply identical headers without importing each other. package imghttp import ( "net/http" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/utils/req" ) // WriteImageHeaders applies the artwork caching contract and reports whether a 304 was written // (the caller must then not write a body). requestedHash is the hash the client asserted, or "". func WriteImageHeaders(w http.ResponseWriter, r *http.Request, img *artwork.Image, requestedHash string) (wrote304 bool) { h := w.Header() // Placeholders are transient stand-ins for unresolved art: never cached, no validators. if img.Placeholder { h.Set("Cache-Control", "no-store") return false } // The ETag versions the served representation, so a re-encoding config change invalidates a // revalidating client's cache; full-size originals are their own pixel hash. etag := img.ETag if etag == "" { etag = img.Hash } // An empty validator would be shared by every such response, 304ing clients that echo it back. if etag != "" { h.Set("ETag", `"`+etag+`"`) } if !img.LastUpdated.IsZero() { h.Set("Last-Modified", img.LastUpdated.UTC().Format(http.TimeFormat)) } // Immutable only when the client asked for the current pixel hash; anything else revalidates, // so art that gets re-resolved is never pinned. if requestedHash != "" && requestedHash == img.Hash { h.Set("Cache-Control", "public, max-age=31536000, immutable") } else { h.Set("Cache-Control", "public, no-cache") } if etag != "" && req.IfNoneMatch(r, etag) { w.WriteHeader(http.StatusNotModified) return true } return false }