Skip to content

Commit d62773c

Browse files
committed
Support stale cache entries
To support eTag/change detection for entries marked stale outside of this library. Also switch the values in XETag1 and XETag2 so they are ordered by old/new.
1 parent d79f2a3 commit d62773c

File tree

2 files changed

+91
-26
lines changed

2 files changed

+91
-26
lines changed

‎httpcache.go‎

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ const (
3333
XETag1 = xEtags + "1"
3434

3535
// XETag2 is the key for the second eTag value.
36+
// Note that in the cache, XETag1 and XETag2 will always be the same.
37+
// In the Response returned from Response, XETag1 will be the cached value (old) and
38+
// XETag2 will be the eTag value from the server (new).
3639
XETag2 = xEtags + "2"
3740
)
3841

3942
// A Cache interface is used by the Transport to store and retrieve responses.
4043
type Cache interface {
4144
// Get returns the []byte representation of a cached response and a bool
42-
// set to true if the value isn't empty
45+
// set to set to false if the key is not found or the value is stale.
4346
Get(key string) (responseBytes []byte, ok bool)
4447
// Set stores the []byte representation of a response against a key
4548
Set(key string, responseBytes []byte)
@@ -65,16 +68,19 @@ func (t *Transport) cacheKey(req *http.Request) string {
6568
}
6669
}
6770

68-
// cachedResponse returns the cached http.Response for req if present, and nil
69-
// otherwise.
70-
func (t *Transport) cachedResponse(req *http.Request) (resp *http.Response, err error) {
71+
// cachedResponse returns the cached http.Response for req if present and
72+
// a bool set to false if the value is stale.
73+
func (t *Transport) cachedResponse(req *http.Request) (*http.Response, bool, error) {
7174
cachedVal, ok := t.Cache.Get(t.cacheKey(req))
72-
if !ok {
73-
return
75+
if !ok && len(cachedVal) == 0 {
76+
return nil, false, nil
7477
}
75-
7678
b := bytes.NewBuffer(cachedVal)
77-
return http.ReadResponse(bufio.NewReader(b), req)
79+
resp, err := http.ReadResponse(bufio.NewReader(b), req)
80+
if err != nil {
81+
return nil, false, err
82+
}
83+
return resp, ok, nil
7884
}
7985

8086
// Transport is an implementation of http.RoundTripper that will return values from a cache
@@ -145,10 +151,13 @@ func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error
145151

146152
cacheable := cacheKey != ""
147153

148-
var cachedResp *http.Response
154+
var (
155+
cachedResp *http.Response
156+
hasCachedResp bool
157+
)
149158
if cacheable {
150-
cachedResp, err = t.cachedResponse(req)
151-
if err == nil && cachedResp != nil && t.AlwaysUseCachedResponse != nil && t.AlwaysUseCachedResponse(req, cacheKey) {
159+
cachedResp, hasCachedResp, err = t.cachedResponse(req)
160+
if err == nil && hasCachedResp && t.AlwaysUseCachedResponse != nil && t.AlwaysUseCachedResponse(req, cacheKey) {
152161
return cachedResp, nil
153162
}
154163
} else {
@@ -161,13 +170,16 @@ func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error
161170
transport = http.DefaultTransport
162171
}
163172

164-
if cacheable && cachedResp != nil && err == nil {
165-
if t.MarkCachedResponses {
166-
cachedResp.Header.Set(XFromCache, "1")
167-
}
173+
if cachedResp != nil {
168174
if t.EnableETagPair {
169175
cachedXEtag, _ = getXETags(cachedResp.Header)
170176
}
177+
}
178+
179+
if cacheable && hasCachedResp && err == nil {
180+
if t.MarkCachedResponses {
181+
cachedResp.Header.Set(XFromCache, "1")
182+
}
171183

172184
if varyMatches(cachedResp, req) {
173185
// Can only use cached value if the new request doesn't Vary significantly
@@ -247,16 +259,19 @@ func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error
247259
t.Cache.Set(cacheKey, respBytes)
248260
}
249261
default:
250-
var etagHash hash.Hash
262+
var (
263+
etagHash hash.Hash
264+
etag1 = cachedXEtag
265+
etag2 string
266+
)
267+
251268
r := resp.Body
252269
if t.EnableETagPair {
253270
if etag := resp.Header.Get("etag"); etag != "" {
254-
resp.Header.Set(XETag1, etag)
255-
etag2 := cachedXEtag
271+
etag1 = etag
256272
if etag2 == "" {
257273
etag2 = etag
258274
}
259-
resp.Header.Set(XETag2, etag2)
260275
} else {
261276
etagHash = md5.New()
262277
r = struct {
@@ -274,17 +289,23 @@ func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error
274289
OnEOF: func(r io.Reader) {
275290
if etagHash != nil {
276291
md5Str := hex.EncodeToString(etagHash.Sum(nil))
292+
etag2 = md5Str
277293
resp.Header.Set(XETag1, md5Str)
278-
etag2 := cachedXEtag
279-
if etag2 == "" {
280-
etag2 = md5Str
294+
resp.Header.Set(XETag2, md5Str)
295+
if etag1 == "" {
296+
etag1 = md5Str
281297
}
282-
resp.Header.Set(XETag2, etag2)
298+
} else {
299+
resp.Header.Set(XETag1, etag1)
300+
resp.Header.Set(XETag2, etag1)
283301
}
302+
284303
resp := *resp
285304
resp.Body = io.NopCloser(r)
286305
respBytes, err := httputil.DumpResponse(&resp, true)
287306
if err == nil {
307+
// Signal any change back to the caller.
308+
resp.Header.Set(XETag1, etag1)
288309
t.Cache.Set(cacheKey, respBytes)
289310
}
290311
},

‎httpcache_test.go‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,9 @@ func TestEnableETagPair(t *testing.T) {
245245
{
246246
_, resp := doMethod(t, "GET", "/helloheaderasbody", map[string]string{"Hello": "world2"})
247247
c.Assert(resp.StatusCode, qt.Equals, http.StatusOK)
248-
c.Assert(resp.Header.Get(XETag1), qt.Equals, "61b7d44bc024f189195b549bf094fbe8")
249-
c.Assert(resp.Header.Get(XETag2), qt.Equals, "48b21a691481958c34cc165011bdb9bc")
248+
c.Assert(resp.Header.Get(XETag1), qt.Equals, "48b21a691481958c34cc165011bdb9bc")
249+
c.Assert(resp.Header.Get(XETag2), qt.Equals, "61b7d44bc024f189195b549bf094fbe8")
250+
250251
}
251252
}
252253

@@ -277,7 +278,6 @@ func TestShouldCache(t *testing.T) {
277278
s.transport.AlwaysUseCachedResponse = func(req *http.Request, key string) bool {
278279
return true
279280
}
280-
281281
s.transport.ShouldCache = func(req *http.Request, resp *http.Response, key string) bool {
282282
return req.Header.Get("Hello") == "world2"
283283
}
@@ -295,6 +295,28 @@ func TestShouldCache(t *testing.T) {
295295
}
296296
}
297297

298+
func TestStaleCachedResponse(t *testing.T) {
299+
resetTest()
300+
s.transport.Cache = &staleCache{}
301+
s.transport.AlwaysUseCachedResponse = func(req *http.Request, key string) bool {
302+
return true
303+
}
304+
s.transport.EnableETagPair = true
305+
c := qt.New(t)
306+
{
307+
_, resp := doMethod(t, "GET", "/helloheaderasbody", map[string]string{"Hello": "world1"})
308+
c.Assert(resp.StatusCode, qt.Equals, http.StatusOK)
309+
c.Assert(resp.Header.Get(XETag1), qt.Equals, "48b21a691481958c34cc165011bdb9bc")
310+
c.Assert(resp.Header.Get(XETag2), qt.Equals, "48b21a691481958c34cc165011bdb9bc")
311+
}
312+
{
313+
_, resp := doMethod(t, "GET", "/helloheaderasbody", map[string]string{"Hello": "world2"})
314+
c.Assert(resp.StatusCode, qt.Equals, http.StatusOK)
315+
c.Assert(resp.Header.Get(XETag1), qt.Equals, "48b21a691481958c34cc165011bdb9bc")
316+
c.Assert(resp.Header.Get(XETag2), qt.Equals, "61b7d44bc024f189195b549bf094fbe8")
317+
}
318+
}
319+
298320
func TestAround(t *testing.T) {
299321
resetTest()
300322
c := qt.New(t)
@@ -1420,3 +1442,25 @@ func (c *memoryCache) Delete(key string) {
14201442
delete(c.items, key)
14211443
c.mu.Unlock()
14221444
}
1445+
1446+
var _ Cache = &staleCache{}
1447+
1448+
type staleCache struct {
1449+
val []byte
1450+
}
1451+
1452+
func (c *staleCache) Get(key string) ([]byte, bool) {
1453+
return c.val, false
1454+
}
1455+
1456+
func (c *staleCache) Set(key string, resp []byte) {
1457+
c.val = resp
1458+
}
1459+
1460+
func (c *staleCache) Delete(key string) {
1461+
c.val = nil
1462+
}
1463+
1464+
func (c *staleCache) Size() int {
1465+
return 1
1466+
}

0 commit comments

Comments
 (0)