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}