main pico / pkg / httpcache / rw.go
Antonio Mika  ·  2026-10-09
  1package httpcache
  2
  3import (
  4	"encoding/binary"
  5	"encoding/json"
  6	"errors"
  7	"net/http"
  8	"strconv"
  9	"strings"
 10	"time"
 11)
 12
 13// responseWriter buffers an upstream response so it can be stored. A body
 14// larger than maxBody is not stored: what was buffered is sent and the rest
 15// streams straight through.
 16type responseWriter struct {
 17	http.ResponseWriter
 18	statusCode int
 19	body       []byte
 20	maxBody    int64
 21	streaming  bool
 22	// streamStatus is the cache-status sent with a streamed response.
 23	streamStatus string
 24}
 25
 26func (rw *responseWriter) WriteHeader(code int) {
 27	rw.statusCode = code
 28	size, err := strconv.ParseInt(rw.Header().Get("content-length"), 10, 64)
 29	if err == nil && rw.tooLarge(size) {
 30		_ = rw.stream()
 31	}
 32}
 33
 34func (rw *responseWriter) Write(data []byte) (int, error) {
 35	if !rw.streaming && rw.tooLarge(int64(len(rw.body)+len(data))) {
 36		if err := rw.stream(); err != nil {
 37			return 0, err
 38		}
 39	}
 40	if rw.streaming {
 41		return rw.ResponseWriter.Write(data)
 42	}
 43	rw.body = append(rw.body, data...)
 44	return len(data), nil
 45}
 46
 47// Flush passes through once the response is streaming. Flushing a buffered
 48// response would send its headers before it is known whether it is stored.
 49func (rw *responseWriter) Flush() {
 50	if rw.streaming {
 51		_ = http.NewResponseController(rw.ResponseWriter).Flush()
 52	}
 53}
 54
 55func (rw *responseWriter) tooLarge(size int64) bool {
 56	return rw.maxBody > 0 && size > rw.maxBody
 57}
 58
 59func (rw *responseWriter) stream() error {
 60	if rw.streaming {
 61		return nil
 62	}
 63	rw.streaming = true
 64	rw.Header().Set("cache-status", rw.streamStatus)
 65	rw.ResponseWriter.WriteHeader(rw.StatusCode())
 66	body := rw.body
 67	rw.body = nil
 68	if len(body) == 0 {
 69		return nil
 70	}
 71	_, err := rw.ResponseWriter.Write(body)
 72	return err
 73}
 74
 75// Body returns the captured response body.
 76func (rw *responseWriter) Body() []byte {
 77	return rw.body
 78}
 79
 80// StatusCode returns the captured status code.
 81func (rw *responseWriter) StatusCode() int {
 82	if rw.statusCode == 0 {
 83		return http.StatusOK
 84	}
 85	return rw.statusCode
 86}
 87
 88func (rw *responseWriter) Send() {
 89	rw.ResponseWriter.WriteHeader(rw.StatusCode())
 90	// RFC 9110 15.4.5: 304 responses MUST NOT contain a body.
 91	if rw.StatusCode() == http.StatusNotModified {
 92		return
 93	}
 94	_, _ = rw.ResponseWriter.Write(rw.body)
 95}
 96
 97func (rw *responseWriter) ToCacheValue(r *http.Request) *CacheValue {
 98	// Normalize header keys to lowercase to avoid case-sensitivity issues
 99	// in the cached map (e.g., "ETag" vs "Etag" as separate keys).
100	headers := make(map[string][]string)
101	for k, v := range rw.Header() {
102		headers[strings.ToLower(k)] = v
103	}
104
105	// Snapshot the request header values named by the response Vary header so
106	// matchVary can compare them on future cache lookups (Vary lists request
107	// header names, not response header names).
108	varyReqHdrs := make(map[string]string)
109	if vary := headers["vary"]; len(vary) > 0 {
110		for _, field := range strings.FieldsFunc(vary[0], func(c rune) bool { return c == ',' }) {
111			field = strings.TrimSpace(strings.ToLower(field))
112			if field != "" && field != "*" {
113				varyReqHdrs[field] = r.Header.Get(field)
114			}
115		}
116	}
117
118	cv := &CacheValue{
119		Header:             headers,
120		Body:               rw.body,
121		CreatedAt:          time.Now(),
122		StatusCode:         rw.StatusCode(),
123		VaryRequestHeaders: varyReqHdrs,
124	}
125
126	return cv
127}
128
129type CacheValue struct {
130	Header             map[string][]string `json:"headers"`
131	Body               []byte              `json:"-"`
132	CreatedAt          time.Time           `json:"created_at"`
133	StatusCode         int                 `json:"status_code"`
134	VaryRequestHeaders map[string]string   `json:"vary_request_headers,omitempty"`
135}
136
137// encode stores the body after the JSON metadata so a cache hit decodes only
138// the metadata.
139func (cv *CacheValue) encode() []byte {
140	meta, _ := json.Marshal(cv)
141	out := make([]byte, 0, binary.MaxVarintLen64+len(meta)+len(cv.Body))
142	out = binary.AppendUvarint(out, uint64(len(meta)))
143	out = append(out, meta...)
144	return append(out, cv.Body...)
145}
146
147// decodeCacheValue reads an entry written by encode. The body shares memory
148// with data, which the cache never modifies.
149func decodeCacheValue(data []byte) (*CacheValue, error) {
150	n, k := binary.Uvarint(data)
151	if k <= 0 || n > uint64(len(data)-k) {
152		return nil, errors.New("corrupt cache entry")
153	}
154	cv := &CacheValue{}
155	if err := json.Unmarshal(data[k:k+int(n)], cv); err != nil {
156		return nil, err
157	}
158	cv.Body = data[k+int(n):]
159	return cv, nil
160}