Skip to content

Commit c5dedbf

Browse files
committed
Cache busting
1 parent 3f90b8c commit c5dedbf

File tree

5 files changed

+298
-65
lines changed

5 files changed

+298
-65
lines changed

pkg/handler/handler.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ package handler
1919
import (
2020
"bytes"
2121
"compress/gzip"
22+
"crypto/sha512"
2223
"encoding/json"
24+
"fmt"
2325
"mime"
2426
"net/http"
27+
"strconv"
2528
"sync"
2629
"time"
2730

@@ -48,6 +51,13 @@ const (
4851
mimePbGz = "application/x-gzip"
4952
)
5053

54+
func computeETag(data []byte) string {
55+
if data == nil {
56+
return ""
57+
}
58+
return fmt.Sprintf("%X", sha512.Sum512(data))
59+
}
60+
5161
// OpenAPIService is the service responsible for serving OpenAPI spec. It has
5262
// the ability to safely change the spec while serving it.
5363
type OpenAPIService struct {
@@ -58,6 +68,7 @@ type OpenAPIService struct {
5868

5969
jsonCache handler.HandlerCache
6070
protoCache handler.HandlerCache
71+
etagCache handler.HandlerCache
6172
}
6273

6374
func init() {
@@ -78,21 +89,29 @@ func NewOpenAPIService(spec *spec.Swagger) (*OpenAPIService, error) {
7889
func (o *OpenAPIService) getSwaggerBytes() ([]byte, string, time.Time, error) {
7990
o.rwMutex.RLock()
8091
defer o.rwMutex.RUnlock()
81-
specBytes, etag, err := o.jsonCache.Get()
92+
specBytes, err := o.jsonCache.Get()
8293
if err != nil {
8394
return nil, "", time.Time{}, err
8495
}
85-
return specBytes, etag, o.lastModified, nil
96+
etagBytes, err := o.etagCache.Get()
97+
if err != nil {
98+
return nil, "", time.Time{}, err
99+
}
100+
return specBytes, string(etagBytes), o.lastModified, nil
86101
}
87102

88103
func (o *OpenAPIService) getSwaggerPbBytes() ([]byte, string, time.Time, error) {
89104
o.rwMutex.RLock()
90105
defer o.rwMutex.RUnlock()
91-
specPb, etag, err := o.protoCache.Get()
106+
specPb, err := o.protoCache.Get()
92107
if err != nil {
93108
return nil, "", time.Time{}, err
94109
}
95-
return specPb, etag, o.lastModified, nil
110+
etagBytes, err := o.etagCache.Get()
111+
if err != nil {
112+
return nil, "", time.Time{}, err
113+
}
114+
return specPb, string(etagBytes), o.lastModified, nil
96115
}
97116

98117
func (o *OpenAPIService) UpdateSpec(openapiSpec *spec.Swagger) (err error) {
@@ -102,12 +121,19 @@ func (o *OpenAPIService) UpdateSpec(openapiSpec *spec.Swagger) (err error) {
102121
return json.Marshal(openapiSpec)
103122
})
104123
o.protoCache = o.protoCache.New(func() ([]byte, error) {
105-
json, _, err := o.jsonCache.Get()
124+
json, err := o.jsonCache.Get()
106125
if err != nil {
107126
return nil, err
108127
}
109128
return ToProtoBinary(json)
110129
})
130+
o.etagCache = o.etagCache.New(func() ([]byte, error) {
131+
json, err := o.jsonCache.Get()
132+
if err != nil {
133+
return nil, err
134+
}
135+
return []byte(computeETag(json)), nil
136+
})
111137
o.lastModified = time.Now()
112138

113139
return nil
@@ -220,7 +246,8 @@ func (o *OpenAPIService) RegisterOpenAPIVersionedService(servePath string, handl
220246
return
221247
}
222248
}
223-
w.Header().Set("Etag", etag)
249+
// ETag must be enclosed in double quotes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
250+
w.Header().Set("Etag", strconv.Quote(etag))
224251
// ServeContent will take care of caching using eTag.
225252
http.ServeContent(w, r, servePath, lastModified, bytes.NewReader(data))
226253
return

pkg/handler3/handler.go

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import (
2323
"fmt"
2424
"mime"
2525
"net/http"
26+
"net/url"
27+
"path"
2628
"sort"
29+
"strconv"
2730
"strings"
2831
"sync"
2932
"time"
@@ -49,9 +52,20 @@ const (
4952
subTypeJSON = "json"
5053
)
5154

55+
// OpenAPIV3Discovery is the format of the Discovery document for OpenAPI V3
56+
// It maps Discovery paths to their corresponding URLs with a hash parameter included
57+
type OpenAPIV3Discovery struct {
58+
Paths map[string]OpenAPIV3DiscoveryGroupVersion
59+
}
60+
61+
// OpenAPIV3DiscoveryGroupVersion includes information about a group version and URL
62+
// for accessing the OpenAPI. The URL includes a hash parameter to support client side caching
63+
type OpenAPIV3DiscoveryGroupVersion struct {
64+
URL string
65+
}
66+
5267
// OpenAPIService is the service responsible for serving OpenAPI spec. It has
5368
// the ability to safely change the spec while serving it.
54-
// OpenAPI V3 currently does not use the lazy marshaling strategy that OpenAPI V2 is using
5569
type OpenAPIService struct {
5670
// rwMutex protects All members of this service.
5771
rwMutex sync.RWMutex
@@ -66,6 +80,7 @@ type OpenAPIV3Group struct {
6680

6781
pbCache handler.HandlerCache
6882
jsonCache handler.HandlerCache
83+
etagCache handler.HandlerCache
6984
}
7085

7186
func init() {
@@ -75,7 +90,18 @@ func init() {
7590
}
7691

7792
func computeETag(data []byte) string {
78-
return fmt.Sprintf("\"%X\"", sha512.Sum512(data))
93+
if data == nil {
94+
return ""
95+
}
96+
return fmt.Sprintf("%X", sha512.Sum512(data))
97+
}
98+
99+
func constructURL(gvString, etag string) string {
100+
u := url.URL{Path: path.Join("/openapi/v3", gvString)}
101+
query := url.Values{}
102+
query.Set("hash", etag)
103+
u.RawQuery = query.Encode()
104+
return u.String()
79105
}
80106

81107
// NewOpenAPIService builds an OpenAPIService starting with the given spec.
@@ -96,10 +122,17 @@ func (o *OpenAPIService) getGroupBytes() ([]byte, error) {
96122
}
97123

98124
sort.Strings(keys)
99-
group := make(map[string][]string)
100-
group["Paths"] = keys
101-
102-
j, err := json.Marshal(group)
125+
discovery := &OpenAPIV3Discovery{Paths: make(map[string]OpenAPIV3DiscoveryGroupVersion)}
126+
for gvString, groupVersion := range o.v3Schema {
127+
etagBytes, err := groupVersion.etagCache.Get()
128+
if err != nil {
129+
return nil, err
130+
}
131+
discovery.Paths[gvString] = OpenAPIV3DiscoveryGroupVersion{
132+
URL: constructURL(gvString, string(etagBytes)),
133+
}
134+
}
135+
j, err := json.Marshal(discovery)
103136
if err != nil {
104137
return nil, err
105138
}
@@ -114,11 +147,19 @@ func (o *OpenAPIService) getSingleGroupBytes(getType string, group string) ([]by
114147
return nil, "", time.Now(), fmt.Errorf("Cannot find CRD group %s", group)
115148
}
116149
if getType == subTypeJSON {
117-
specBytes, etag, err := v.jsonCache.Get()
118-
return specBytes, etag, v.lastModified, err
150+
specBytes, err := v.jsonCache.Get()
151+
if err != nil {
152+
return nil, "", v.lastModified, err
153+
}
154+
etagBytes, err := v.etagCache.Get()
155+
return specBytes, string(etagBytes), v.lastModified, err
119156
} else if getType == subTypeProtobuf {
120-
specPb, etag, err := v.pbCache.Get()
121-
return specPb, etag, v.lastModified, err
157+
specPb, err := v.pbCache.Get()
158+
if err != nil {
159+
return nil, "", v.lastModified, err
160+
}
161+
etagBytes, err := v.etagCache.Get()
162+
return specPb, string(etagBytes), v.lastModified, err
122163
}
123164
return nil, "", time.Now(), fmt.Errorf("Invalid accept clause %s", getType)
124165
}
@@ -127,15 +168,10 @@ func (o *OpenAPIService) UpdateGroupVersion(group string, openapi *spec3.OpenAPI
127168
o.rwMutex.Lock()
128169
defer o.rwMutex.Unlock()
129170

130-
specBytes, err := json.Marshal(openapi)
131-
if err != nil {
132-
return err
133-
}
134-
135171
if _, ok := o.v3Schema[group]; !ok {
136172
o.v3Schema[group] = &OpenAPIV3Group{}
137173
}
138-
return o.v3Schema[group].UpdateSpec(specBytes)
174+
return o.v3Schema[group].UpdateSpec(openapi)
139175
}
140176

141177
func (o *OpenAPIService) DeleteGroupVersion(group string) {
@@ -157,6 +193,21 @@ func (o *OpenAPIService) HandleDiscovery(w http.ResponseWriter, r *http.Request)
157193
http.ServeContent(w, r, "/openapi/v3", time.Now(), bytes.NewReader(data))
158194
}
159195

196+
func applyCacheBusting(w http.ResponseWriter, r *http.Request, etag string) {
197+
hash := r.URL.Query().Get("hash")
198+
// Only apply cache busting on requests where the hash is provided.
199+
if hash != "" {
200+
// The Vary header is required because the Accept header can change the contents returned. This prevents clients from caching protobuf as JSON and vice versa.
201+
w.Header().Set("Vary", "*")
202+
203+
if hash == etag {
204+
// Set the Expires directive to one year from the request, effectively indicating that the cache never expires.
205+
w.Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(time.RFC1123))
206+
w.Header().Set("Cache-Control", "public, immutable")
207+
}
208+
}
209+
}
210+
160211
func (o *OpenAPIService) HandleGroupVersion(w http.ResponseWriter, r *http.Request) {
161212
url := strings.SplitAfterN(r.URL.Path, "/", 4)
162213
group := url[3]
@@ -192,7 +243,15 @@ func (o *OpenAPIService) HandleGroupVersion(w http.ResponseWriter, r *http.Reque
192243
if err != nil {
193244
return
194245
}
195-
w.Header().Set("Etag", etag)
246+
// ETag must be enclosed in double quotes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
247+
w.Header().Set("Etag", strconv.Quote(etag))
248+
249+
if hash := r.URL.Query().Get("hash"); hash != "" && hash != etag {
250+
u := constructURL(group, etag)
251+
http.Redirect(w, r, u, 301)
252+
return
253+
}
254+
applyCacheBusting(w, r, etag)
196255
http.ServeContent(w, r, "", lastModified, bytes.NewReader(data))
197256
return
198257
}
@@ -207,16 +266,26 @@ func (o *OpenAPIService) RegisterOpenAPIV3VersionedService(servePath string, han
207266
return nil
208267
}
209268

210-
func (o *OpenAPIV3Group) UpdateSpec(specBytes []byte) (err error) {
269+
func (o *OpenAPIV3Group) UpdateSpec(openapi *spec3.OpenAPI) (err error) {
211270
o.rwMutex.Lock()
212271
defer o.rwMutex.Unlock()
213272

214273
o.pbCache = o.pbCache.New(func() ([]byte, error) {
215-
return ToV3ProtoBinary(specBytes)
274+
json, err := o.jsonCache.Get()
275+
if err != nil {
276+
return nil, err
277+
}
278+
return ToV3ProtoBinary(json)
216279
})
217-
218280
o.jsonCache = o.jsonCache.New(func() ([]byte, error) {
219-
return specBytes, nil
281+
return json.Marshal(openapi)
282+
})
283+
o.etagCache = o.etagCache.New(func() ([]byte, error) {
284+
json, err := o.jsonCache.Get()
285+
if err != nil {
286+
return nil, err
287+
}
288+
return []byte(computeETag(json)), nil
220289
})
221290
o.lastModified = time.Now()
222291
return nil

0 commit comments

Comments
 (0)