repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / httpcache
Eric Bower  ·  2026-04-28

rw.go

 1package httpcache
 2
 3import (
 4	"net/http"
 5	"strings"
 6	"time"
 7)
 8
 9type responseWriter struct {
10	http.ResponseWriter
11	statusCode int
12	body       []byte
13}
14
15func (rw *responseWriter) WriteHeader(code int) {
16	rw.statusCode = code
17}
18
19func (rw *responseWriter) Write(data []byte) (int, error) {
20	rw.body = append(rw.body, data...)
21	return len(data), nil
22}
23
24// Body returns the captured response body.
25func (rw *responseWriter) Body() []byte {
26	return rw.body
27}
28
29// StatusCode returns the captured status code.
30func (rw *responseWriter) StatusCode() int {
31	if rw.statusCode == 0 {
32		return http.StatusOK
33	}
34	return rw.statusCode
35}
36
37func (rw *responseWriter) Send() {
38	rw.ResponseWriter.WriteHeader(rw.StatusCode())
39	// RFC 9110 15.4.5: 304 responses MUST NOT contain a body.
40	if rw.StatusCode() == http.StatusNotModified {
41		return
42	}
43	_, _ = rw.ResponseWriter.Write(rw.body)
44}
45
46func (rw *responseWriter) ToCacheValue(r *http.Request) *CacheValue {
47	// Normalize header keys to lowercase to avoid case-sensitivity issues
48	// in the cached map (e.g., "ETag" vs "Etag" as separate keys).
49	headers := make(map[string][]string)
50	for k, v := range rw.Header() {
51		headers[strings.ToLower(k)] = v
52	}
53
54	// Snapshot the request header values named by the response Vary header so
55	// matchVary can compare them on future cache lookups (Vary lists request
56	// header names, not response header names).
57	varyReqHdrs := make(map[string]string)
58	if vary := headers["vary"]; len(vary) > 0 {
59		for _, field := range strings.FieldsFunc(vary[0], func(c rune) bool { return c == ',' }) {
60			field = strings.TrimSpace(strings.ToLower(field))
61			if field != "" && field != "*" {
62				varyReqHdrs[field] = r.Header.Get(field)
63			}
64		}
65	}
66
67	cv := &CacheValue{
68		Header:             headers,
69		Body:               rw.body,
70		CreatedAt:          time.Now(),
71		StatusCode:         rw.StatusCode(),
72		VaryRequestHeaders: varyReqHdrs,
73	}
74
75	return cv
76}
77
78type CacheValue struct {
79	Header             map[string][]string `json:"headers"`
80	Body               []byte              `json:"body"`
81	CreatedAt          time.Time           `json:"created_at"`
82	StatusCode         int                 `json:"status_code"`
83	VaryRequestHeaders map[string]string   `json:"vary_request_headers,omitempty"`
84}