mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-02 01:38:17 +00:00
63 lines
1.9 KiB
Go
63 lines
1.9 KiB
Go
package web
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ServeJSONCached marshals v to JSON and serves it with a short public cache
|
|
// window plus a content-derived ETag, replying 304 Not Modified when the
|
|
// client's If-None-Match already matches. It is for small, public, infrequently
|
|
// changing payloads (e.g. the org map's repeater points): the ETag lets repeat
|
|
// visitors and CDNs revalidate cheaply instead of re-downloading, and max-age
|
|
// caps how stale a change can be.
|
|
//
|
|
// It returns any marshal error before writing a status so the caller can log it
|
|
// via ServerError; once headers/body are written it returns nil.
|
|
func ServeJSONCached(w http.ResponseWriter, r *http.Request, maxAge time.Duration, v any) error {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sum := sha256.Sum256(b)
|
|
// A strong, quoted ETag over the exact bytes served (RFC 7232). 128 bits of
|
|
// the digest is ample to make collisions irrelevant here.
|
|
etag := `"` + hex.EncodeToString(sum[:16]) + `"`
|
|
|
|
h := w.Header()
|
|
h.Set("Content-Type", "application/json; charset=utf-8")
|
|
h.Set("Cache-Control", "public, max-age="+strconv.Itoa(int(maxAge.Seconds())))
|
|
h.Set("ETag", etag)
|
|
|
|
if matchesETag(r.Header.Get("If-None-Match"), etag) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return nil
|
|
}
|
|
_, _ = w.Write(b)
|
|
return nil
|
|
}
|
|
|
|
// matchesETag reports whether the If-None-Match header (a comma-separated list,
|
|
// possibly "*" or weak "W/"-prefixed validators) matches etag. We compare
|
|
// ignoring the weak prefix, which is safe for a cache-revalidation check.
|
|
func matchesETag(ifNoneMatch, etag string) bool {
|
|
if ifNoneMatch == "" {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(ifNoneMatch) == "*" {
|
|
return true
|
|
}
|
|
want := strings.TrimPrefix(etag, "W/")
|
|
for _, part := range strings.Split(ifNoneMatch, ",") {
|
|
if strings.TrimPrefix(strings.TrimSpace(part), "W/") == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|