├── .travis.yml ├── LICENSE.txt ├── README.md ├── diskcache ├── diskcache.go └── diskcache_test.go ├── httpcache.go ├── httpcache_test.go ├── leveldbcache ├── leveldbcache.go └── leveldbcache_test.go ├── memcache ├── appengine.go ├── appengine_test.go ├── memcache.go └── memcache_test.go ├── redis ├── redis.go └── redis_test.go └── test ├── test.go └── test_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | matrix: 4 | allow_failures: 5 | - go: master 6 | fast_finish: true 7 | include: 8 | - go: 1.10.x 9 | - go: 1.11.x 10 | env: GOFMT=1 11 | - go: master 12 | install: 13 | - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). 14 | script: 15 | - go get -t -v ./... 16 | - if test -n "${GOFMT}"; then gofmt -w -s . && git diff --exit-code; fi 17 | - go tool vet . 18 | - go test -v -race ./... 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2012 Greg Jones (greg.jones@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | httpcache 2 | ========= 3 | 4 | [![Build Status](https://travis-ci.org/gregjones/httpcache.svg?branch=master)](https://travis-ci.org/gregjones/httpcache) [![GoDoc](https://godoc.org/github.com/gregjones/httpcache?status.svg)](https://godoc.org/github.com/gregjones/httpcache) 5 | 6 | Package httpcache provides a http.RoundTripper implementation that works as a mostly [RFC 7234](https://tools.ietf.org/html/rfc7234) compliant cache for http responses. 7 | 8 | It is only suitable for use as a 'private' cache (i.e. for a web-browser or an API-client and not for a shared proxy). 9 | 10 | This project isn't actively maintained; it works for what I, and seemingly others, want to do with it, and I consider it "done". That said, if you find any issues, please open a Pull Request and I will try to review it. Any changes now that change the public API won't be considered. 11 | 12 | Cache Backends 13 | -------------- 14 | 15 | - The built-in 'memory' cache stores responses in an in-memory map. 16 | - [`github.com/gregjones/httpcache/diskcache`](https://github.com/gregjones/httpcache/tree/master/diskcache) provides a filesystem-backed cache using the [diskv](https://github.com/peterbourgon/diskv) library. 17 | - [`github.com/gregjones/httpcache/memcache`](https://github.com/gregjones/httpcache/tree/master/memcache) provides memcache implementations, for both App Engine and 'normal' memcache servers. 18 | - [`sourcegraph.com/sourcegraph/s3cache`](https://sourcegraph.com/github.com/sourcegraph/s3cache) uses Amazon S3 for storage. 19 | - [`github.com/gregjones/httpcache/leveldbcache`](https://github.com/gregjones/httpcache/tree/master/leveldbcache) provides a filesystem-backed cache using [leveldb](https://github.com/syndtr/goleveldb/leveldb). 20 | - [`github.com/die-net/lrucache`](https://github.com/die-net/lrucache) provides an in-memory cache that will evict least-recently used entries. 21 | - [`github.com/die-net/lrucache/twotier`](https://github.com/die-net/lrucache/tree/master/twotier) allows caches to be combined, for example to use lrucache above with a persistent disk-cache. 22 | - [`github.com/birkelund/boltdbcache`](https://github.com/birkelund/boltdbcache) provides a BoltDB implementation (based on the [bbolt](https://github.com/coreos/bbolt) fork). 23 | 24 | If you implement any other backend and wish it to be linked here, please send a PR editing this file. 25 | 26 | License 27 | ------- 28 | 29 | - [MIT License](LICENSE.txt) 30 | -------------------------------------------------------------------------------- /diskcache/diskcache.go: -------------------------------------------------------------------------------- 1 | // Package diskcache provides an implementation of httpcache.Cache that uses the diskv package 2 | // to supplement an in-memory map with persistent storage 3 | // 4 | package diskcache 5 | 6 | import ( 7 | "bytes" 8 | "crypto/md5" 9 | "encoding/hex" 10 | "github.com/peterbourgon/diskv" 11 | "io" 12 | ) 13 | 14 | // Cache is an implementation of httpcache.Cache that supplements the in-memory map with persistent storage 15 | type Cache struct { 16 | d *diskv.Diskv 17 | } 18 | 19 | // Get returns the response corresponding to key if present 20 | func (c *Cache) Get(key string) (resp []byte, ok bool) { 21 | key = keyToFilename(key) 22 | resp, err := c.d.Read(key) 23 | if err != nil { 24 | return []byte{}, false 25 | } 26 | return resp, true 27 | } 28 | 29 | // Set saves a response to the cache as key 30 | func (c *Cache) Set(key string, resp []byte) { 31 | key = keyToFilename(key) 32 | c.d.WriteStream(key, bytes.NewReader(resp), true) 33 | } 34 | 35 | // Delete removes the response with key from the cache 36 | func (c *Cache) Delete(key string) { 37 | key = keyToFilename(key) 38 | c.d.Erase(key) 39 | } 40 | 41 | func keyToFilename(key string) string { 42 | h := md5.New() 43 | io.WriteString(h, key) 44 | return hex.EncodeToString(h.Sum(nil)) 45 | } 46 | 47 | // New returns a new Cache that will store files in basePath 48 | func New(basePath string) *Cache { 49 | return &Cache{ 50 | d: diskv.New(diskv.Options{ 51 | BasePath: basePath, 52 | CacheSizeMax: 100 * 1024 * 1024, // 100MB 53 | }), 54 | } 55 | } 56 | 57 | // NewWithDiskv returns a new Cache using the provided Diskv as underlying 58 | // storage. 59 | func NewWithDiskv(d *diskv.Diskv) *Cache { 60 | return &Cache{d} 61 | } 62 | -------------------------------------------------------------------------------- /diskcache/diskcache_test.go: -------------------------------------------------------------------------------- 1 | package diskcache 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gregjones/httpcache/test" 9 | ) 10 | 11 | func TestDiskCache(t *testing.T) { 12 | tempDir, err := ioutil.TempDir("", "httpcache") 13 | if err != nil { 14 | t.Fatalf("TempDir: %v", err) 15 | } 16 | defer os.RemoveAll(tempDir) 17 | 18 | test.Cache(t, New(tempDir)) 19 | } 20 | -------------------------------------------------------------------------------- /httpcache.go: -------------------------------------------------------------------------------- 1 | // Package httpcache provides a http.RoundTripper implementation that works as a 2 | // mostly RFC-compliant cache for http responses. 3 | // 4 | // It is only suitable for use as a 'private' cache (i.e. for a web-browser or an API-client 5 | // and not for a shared proxy). 6 | // 7 | package httpcache 8 | 9 | import ( 10 | "bufio" 11 | "bytes" 12 | "errors" 13 | "io" 14 | "io/ioutil" 15 | "net/http" 16 | "net/http/httputil" 17 | "strings" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | const ( 23 | stale = iota 24 | fresh 25 | transparent 26 | // XFromCache is the header added to responses that are returned from the cache 27 | XFromCache = "X-From-Cache" 28 | ) 29 | 30 | // A Cache interface is used by the Transport to store and retrieve responses. 31 | type Cache interface { 32 | // Get returns the []byte representation of a cached response and a bool 33 | // set to true if the value isn't empty 34 | Get(key string) (responseBytes []byte, ok bool) 35 | // Set stores the []byte representation of a response against a key 36 | Set(key string, responseBytes []byte) 37 | // Delete removes the value associated with the key 38 | Delete(key string) 39 | } 40 | 41 | // cacheKey returns the cache key for req. 42 | func cacheKey(req *http.Request) string { 43 | if req.Method == http.MethodGet { 44 | return req.URL.String() 45 | } else { 46 | return req.Method + " " + req.URL.String() 47 | } 48 | } 49 | 50 | // CachedResponse returns the cached http.Response for req if present, and nil 51 | // otherwise. 52 | func CachedResponse(c Cache, req *http.Request) (resp *http.Response, err error) { 53 | cachedVal, ok := c.Get(cacheKey(req)) 54 | if !ok { 55 | return 56 | } 57 | 58 | b := bytes.NewBuffer(cachedVal) 59 | return http.ReadResponse(bufio.NewReader(b), req) 60 | } 61 | 62 | // MemoryCache is an implemtation of Cache that stores responses in an in-memory map. 63 | type MemoryCache struct { 64 | mu sync.RWMutex 65 | items map[string][]byte 66 | } 67 | 68 | // Get returns the []byte representation of the response and true if present, false if not 69 | func (c *MemoryCache) Get(key string) (resp []byte, ok bool) { 70 | c.mu.RLock() 71 | resp, ok = c.items[key] 72 | c.mu.RUnlock() 73 | return resp, ok 74 | } 75 | 76 | // Set saves response resp to the cache with key 77 | func (c *MemoryCache) Set(key string, resp []byte) { 78 | c.mu.Lock() 79 | c.items[key] = resp 80 | c.mu.Unlock() 81 | } 82 | 83 | // Delete removes key from the cache 84 | func (c *MemoryCache) Delete(key string) { 85 | c.mu.Lock() 86 | delete(c.items, key) 87 | c.mu.Unlock() 88 | } 89 | 90 | // NewMemoryCache returns a new Cache that will store items in an in-memory map 91 | func NewMemoryCache() *MemoryCache { 92 | c := &MemoryCache{items: map[string][]byte{}} 93 | return c 94 | } 95 | 96 | // Transport is an implementation of http.RoundTripper that will return values from a cache 97 | // where possible (avoiding a network request) and will additionally add validators (etag/if-modified-since) 98 | // to repeated requests allowing servers to return 304 / Not Modified 99 | type Transport struct { 100 | // The RoundTripper interface actually used to make requests 101 | // If nil, http.DefaultTransport is used 102 | Transport http.RoundTripper 103 | Cache Cache 104 | // If true, responses returned from the cache will be given an extra header, X-From-Cache 105 | MarkCachedResponses bool 106 | } 107 | 108 | // NewTransport returns a new Transport with the 109 | // provided Cache implementation and MarkCachedResponses set to true 110 | func NewTransport(c Cache) *Transport { 111 | return &Transport{Cache: c, MarkCachedResponses: true} 112 | } 113 | 114 | // Client returns an *http.Client that caches responses. 115 | func (t *Transport) Client() *http.Client { 116 | return &http.Client{Transport: t} 117 | } 118 | 119 | // varyMatches will return false unless all of the cached values for the headers listed in Vary 120 | // match the new request 121 | func varyMatches(cachedResp *http.Response, req *http.Request) bool { 122 | for _, header := range headerAllCommaSepValues(cachedResp.Header, "vary") { 123 | header = http.CanonicalHeaderKey(header) 124 | if header != "" && req.Header.Get(header) != cachedResp.Header.Get("X-Varied-"+header) { 125 | return false 126 | } 127 | } 128 | return true 129 | } 130 | 131 | // RoundTrip takes a Request and returns a Response 132 | // 133 | // If there is a fresh Response already in cache, then it will be returned without connecting to 134 | // the server. 135 | // 136 | // If there is a stale Response, then any validators it contains will be set on the new request 137 | // to give the server a chance to respond with NotModified. If this happens, then the cached Response 138 | // will be returned. 139 | func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 140 | cacheKey := cacheKey(req) 141 | cacheable := (req.Method == "GET" || req.Method == "HEAD") && req.Header.Get("range") == "" 142 | var cachedResp *http.Response 143 | if cacheable { 144 | cachedResp, err = CachedResponse(t.Cache, req) 145 | } else { 146 | // Need to invalidate an existing value 147 | t.Cache.Delete(cacheKey) 148 | } 149 | 150 | transport := t.Transport 151 | if transport == nil { 152 | transport = http.DefaultTransport 153 | } 154 | 155 | if cacheable && cachedResp != nil && err == nil { 156 | if t.MarkCachedResponses { 157 | cachedResp.Header.Set(XFromCache, "1") 158 | } 159 | 160 | if varyMatches(cachedResp, req) { 161 | // Can only use cached value if the new request doesn't Vary significantly 162 | freshness := getFreshness(cachedResp.Header, req.Header) 163 | if freshness == fresh { 164 | return cachedResp, nil 165 | } 166 | 167 | if freshness == stale { 168 | var req2 *http.Request 169 | // Add validators if caller hasn't already done so 170 | etag := cachedResp.Header.Get("etag") 171 | if etag != "" && req.Header.Get("etag") == "" { 172 | req2 = cloneRequest(req) 173 | req2.Header.Set("if-none-match", etag) 174 | } 175 | lastModified := cachedResp.Header.Get("last-modified") 176 | if lastModified != "" && req.Header.Get("last-modified") == "" { 177 | if req2 == nil { 178 | req2 = cloneRequest(req) 179 | } 180 | req2.Header.Set("if-modified-since", lastModified) 181 | } 182 | if req2 != nil { 183 | req = req2 184 | } 185 | } 186 | } 187 | 188 | resp, err = transport.RoundTrip(req) 189 | if err == nil && req.Method == "GET" && resp.StatusCode == http.StatusNotModified { 190 | // Replace the 304 response with the one from cache, but update with some new headers 191 | endToEndHeaders := getEndToEndHeaders(resp.Header) 192 | for _, header := range endToEndHeaders { 193 | cachedResp.Header[header] = resp.Header[header] 194 | } 195 | resp = cachedResp 196 | } else if (err != nil || (cachedResp != nil && resp.StatusCode >= 500)) && 197 | req.Method == "GET" && canStaleOnError(cachedResp.Header, req.Header) { 198 | // In case of transport failure and stale-if-error activated, returns cached content 199 | // when available 200 | return cachedResp, nil 201 | } else { 202 | if err != nil || resp.StatusCode != http.StatusOK { 203 | t.Cache.Delete(cacheKey) 204 | } 205 | if err != nil { 206 | return nil, err 207 | } 208 | } 209 | } else { 210 | reqCacheControl := parseCacheControl(req.Header) 211 | if _, ok := reqCacheControl["only-if-cached"]; ok { 212 | resp = newGatewayTimeoutResponse(req) 213 | } else { 214 | resp, err = transport.RoundTrip(req) 215 | if err != nil { 216 | return nil, err 217 | } 218 | } 219 | } 220 | 221 | if cacheable && canStore(parseCacheControl(req.Header), parseCacheControl(resp.Header)) { 222 | for _, varyKey := range headerAllCommaSepValues(resp.Header, "vary") { 223 | varyKey = http.CanonicalHeaderKey(varyKey) 224 | fakeHeader := "X-Varied-" + varyKey 225 | reqValue := req.Header.Get(varyKey) 226 | if reqValue != "" { 227 | resp.Header.Set(fakeHeader, reqValue) 228 | } 229 | } 230 | switch req.Method { 231 | case "GET": 232 | // Delay caching until EOF is reached. 233 | resp.Body = &cachingReadCloser{ 234 | R: resp.Body, 235 | OnEOF: func(r io.Reader) { 236 | resp := *resp 237 | resp.Body = ioutil.NopCloser(r) 238 | respBytes, err := httputil.DumpResponse(&resp, true) 239 | if err == nil { 240 | t.Cache.Set(cacheKey, respBytes) 241 | } 242 | }, 243 | } 244 | default: 245 | respBytes, err := httputil.DumpResponse(resp, true) 246 | if err == nil { 247 | t.Cache.Set(cacheKey, respBytes) 248 | } 249 | } 250 | } else { 251 | t.Cache.Delete(cacheKey) 252 | } 253 | return resp, nil 254 | } 255 | 256 | // ErrNoDateHeader indicates that the HTTP headers contained no Date header. 257 | var ErrNoDateHeader = errors.New("no Date header") 258 | 259 | // Date parses and returns the value of the Date header. 260 | func Date(respHeaders http.Header) (date time.Time, err error) { 261 | dateHeader := respHeaders.Get("date") 262 | if dateHeader == "" { 263 | err = ErrNoDateHeader 264 | return 265 | } 266 | 267 | return time.Parse(time.RFC1123, dateHeader) 268 | } 269 | 270 | type realClock struct{} 271 | 272 | func (c *realClock) since(d time.Time) time.Duration { 273 | return time.Since(d) 274 | } 275 | 276 | type timer interface { 277 | since(d time.Time) time.Duration 278 | } 279 | 280 | var clock timer = &realClock{} 281 | 282 | // getFreshness will return one of fresh/stale/transparent based on the cache-control 283 | // values of the request and the response 284 | // 285 | // fresh indicates the response can be returned 286 | // stale indicates that the response needs validating before it is returned 287 | // transparent indicates the response should not be used to fulfil the request 288 | // 289 | // Because this is only a private cache, 'public' and 'private' in cache-control aren't 290 | // signficant. Similarly, smax-age isn't used. 291 | func getFreshness(respHeaders, reqHeaders http.Header) (freshness int) { 292 | respCacheControl := parseCacheControl(respHeaders) 293 | reqCacheControl := parseCacheControl(reqHeaders) 294 | if _, ok := reqCacheControl["no-cache"]; ok { 295 | return transparent 296 | } 297 | if _, ok := respCacheControl["no-cache"]; ok { 298 | return stale 299 | } 300 | if _, ok := reqCacheControl["only-if-cached"]; ok { 301 | return fresh 302 | } 303 | 304 | date, err := Date(respHeaders) 305 | if err != nil { 306 | return stale 307 | } 308 | currentAge := clock.since(date) 309 | 310 | var lifetime time.Duration 311 | var zeroDuration time.Duration 312 | 313 | // If a response includes both an Expires header and a max-age directive, 314 | // the max-age directive overrides the Expires header, even if the Expires header is more restrictive. 315 | if maxAge, ok := respCacheControl["max-age"]; ok { 316 | lifetime, err = time.ParseDuration(maxAge + "s") 317 | if err != nil { 318 | lifetime = zeroDuration 319 | } 320 | } else { 321 | expiresHeader := respHeaders.Get("Expires") 322 | if expiresHeader != "" { 323 | expires, err := time.Parse(time.RFC1123, expiresHeader) 324 | if err != nil { 325 | lifetime = zeroDuration 326 | } else { 327 | lifetime = expires.Sub(date) 328 | } 329 | } 330 | } 331 | 332 | if maxAge, ok := reqCacheControl["max-age"]; ok { 333 | // the client is willing to accept a response whose age is no greater than the specified time in seconds 334 | lifetime, err = time.ParseDuration(maxAge + "s") 335 | if err != nil { 336 | lifetime = zeroDuration 337 | } 338 | } 339 | if minfresh, ok := reqCacheControl["min-fresh"]; ok { 340 | // the client wants a response that will still be fresh for at least the specified number of seconds. 341 | minfreshDuration, err := time.ParseDuration(minfresh + "s") 342 | if err == nil { 343 | currentAge = time.Duration(currentAge + minfreshDuration) 344 | } 345 | } 346 | 347 | if maxstale, ok := reqCacheControl["max-stale"]; ok { 348 | // Indicates that the client is willing to accept a response that has exceeded its expiration time. 349 | // If max-stale is assigned a value, then the client is willing to accept a response that has exceeded 350 | // its expiration time by no more than the specified number of seconds. 351 | // If no value is assigned to max-stale, then the client is willing to accept a stale response of any age. 352 | // 353 | // Responses served only because of a max-stale value are supposed to have a Warning header added to them, 354 | // but that seems like a hassle, and is it actually useful? If so, then there needs to be a different 355 | // return-value available here. 356 | if maxstale == "" { 357 | return fresh 358 | } 359 | maxstaleDuration, err := time.ParseDuration(maxstale + "s") 360 | if err == nil { 361 | currentAge = time.Duration(currentAge - maxstaleDuration) 362 | } 363 | } 364 | 365 | if lifetime > currentAge { 366 | return fresh 367 | } 368 | 369 | return stale 370 | } 371 | 372 | // Returns true if either the request or the response includes the stale-if-error 373 | // cache control extension: https://tools.ietf.org/html/rfc5861 374 | func canStaleOnError(respHeaders, reqHeaders http.Header) bool { 375 | respCacheControl := parseCacheControl(respHeaders) 376 | reqCacheControl := parseCacheControl(reqHeaders) 377 | 378 | var err error 379 | lifetime := time.Duration(-1) 380 | 381 | if staleMaxAge, ok := respCacheControl["stale-if-error"]; ok { 382 | if staleMaxAge != "" { 383 | lifetime, err = time.ParseDuration(staleMaxAge + "s") 384 | if err != nil { 385 | return false 386 | } 387 | } else { 388 | return true 389 | } 390 | } 391 | if staleMaxAge, ok := reqCacheControl["stale-if-error"]; ok { 392 | if staleMaxAge != "" { 393 | lifetime, err = time.ParseDuration(staleMaxAge + "s") 394 | if err != nil { 395 | return false 396 | } 397 | } else { 398 | return true 399 | } 400 | } 401 | 402 | if lifetime >= 0 { 403 | date, err := Date(respHeaders) 404 | if err != nil { 405 | return false 406 | } 407 | currentAge := clock.since(date) 408 | if lifetime > currentAge { 409 | return true 410 | } 411 | } 412 | 413 | return false 414 | } 415 | 416 | func getEndToEndHeaders(respHeaders http.Header) []string { 417 | // These headers are always hop-by-hop 418 | hopByHopHeaders := map[string]struct{}{ 419 | "Connection": {}, 420 | "Keep-Alive": {}, 421 | "Proxy-Authenticate": {}, 422 | "Proxy-Authorization": {}, 423 | "Te": {}, 424 | "Trailers": {}, 425 | "Transfer-Encoding": {}, 426 | "Upgrade": {}, 427 | } 428 | 429 | for _, extra := range strings.Split(respHeaders.Get("connection"), ",") { 430 | // any header listed in connection, if present, is also considered hop-by-hop 431 | if strings.Trim(extra, " ") != "" { 432 | hopByHopHeaders[http.CanonicalHeaderKey(extra)] = struct{}{} 433 | } 434 | } 435 | endToEndHeaders := []string{} 436 | for respHeader := range respHeaders { 437 | if _, ok := hopByHopHeaders[respHeader]; !ok { 438 | endToEndHeaders = append(endToEndHeaders, respHeader) 439 | } 440 | } 441 | return endToEndHeaders 442 | } 443 | 444 | func canStore(reqCacheControl, respCacheControl cacheControl) (canStore bool) { 445 | if _, ok := respCacheControl["no-store"]; ok { 446 | return false 447 | } 448 | if _, ok := reqCacheControl["no-store"]; ok { 449 | return false 450 | } 451 | return true 452 | } 453 | 454 | func newGatewayTimeoutResponse(req *http.Request) *http.Response { 455 | var braw bytes.Buffer 456 | braw.WriteString("HTTP/1.1 504 Gateway Timeout\r\n\r\n") 457 | resp, err := http.ReadResponse(bufio.NewReader(&braw), req) 458 | if err != nil { 459 | panic(err) 460 | } 461 | return resp 462 | } 463 | 464 | // cloneRequest returns a clone of the provided *http.Request. 465 | // The clone is a shallow copy of the struct and its Header map. 466 | // (This function copyright goauth2 authors: https://code.google.com/p/goauth2) 467 | func cloneRequest(r *http.Request) *http.Request { 468 | // shallow copy of the struct 469 | r2 := new(http.Request) 470 | *r2 = *r 471 | // deep copy of the Header 472 | r2.Header = make(http.Header) 473 | for k, s := range r.Header { 474 | r2.Header[k] = s 475 | } 476 | return r2 477 | } 478 | 479 | type cacheControl map[string]string 480 | 481 | func parseCacheControl(headers http.Header) cacheControl { 482 | cc := cacheControl{} 483 | ccHeader := headers.Get("Cache-Control") 484 | for _, part := range strings.Split(ccHeader, ",") { 485 | part = strings.Trim(part, " ") 486 | if part == "" { 487 | continue 488 | } 489 | if strings.ContainsRune(part, '=') { 490 | keyval := strings.Split(part, "=") 491 | cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") 492 | } else { 493 | cc[part] = "" 494 | } 495 | } 496 | return cc 497 | } 498 | 499 | // headerAllCommaSepValues returns all comma-separated values (each 500 | // with whitespace trimmed) for header name in headers. According to 501 | // Section 4.2 of the HTTP/1.1 spec 502 | // (http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2), 503 | // values from multiple occurrences of a header should be concatenated, if 504 | // the header's value is a comma-separated list. 505 | func headerAllCommaSepValues(headers http.Header, name string) []string { 506 | var vals []string 507 | for _, val := range headers[http.CanonicalHeaderKey(name)] { 508 | fields := strings.Split(val, ",") 509 | for i, f := range fields { 510 | fields[i] = strings.TrimSpace(f) 511 | } 512 | vals = append(vals, fields...) 513 | } 514 | return vals 515 | } 516 | 517 | // cachingReadCloser is a wrapper around ReadCloser R that calls OnEOF 518 | // handler with a full copy of the content read from R when EOF is 519 | // reached. 520 | type cachingReadCloser struct { 521 | // Underlying ReadCloser. 522 | R io.ReadCloser 523 | // OnEOF is called with a copy of the content of R when EOF is reached. 524 | OnEOF func(io.Reader) 525 | 526 | buf bytes.Buffer // buf stores a copy of the content of R. 527 | } 528 | 529 | // Read reads the next len(p) bytes from R or until R is drained. The 530 | // return value n is the number of bytes read. If R has no data to 531 | // return, err is io.EOF and OnEOF is called with a full copy of what 532 | // has been read so far. 533 | func (r *cachingReadCloser) Read(p []byte) (n int, err error) { 534 | n, err = r.R.Read(p) 535 | r.buf.Write(p[:n]) 536 | if err == io.EOF { 537 | r.OnEOF(bytes.NewReader(r.buf.Bytes())) 538 | } 539 | return n, err 540 | } 541 | 542 | func (r *cachingReadCloser) Close() error { 543 | return r.R.Close() 544 | } 545 | 546 | // NewMemoryCacheTransport returns a new Transport using the in-memory cache implementation 547 | func NewMemoryCacheTransport() *Transport { 548 | c := NewMemoryCache() 549 | t := NewTransport(c) 550 | return t 551 | } 552 | -------------------------------------------------------------------------------- /httpcache_test.go: -------------------------------------------------------------------------------- 1 | package httpcache 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "flag" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "os" 12 | "strconv" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | var s struct { 18 | server *httptest.Server 19 | client http.Client 20 | transport *Transport 21 | done chan struct{} // Closed to unlock infinite handlers. 22 | } 23 | 24 | type fakeClock struct { 25 | elapsed time.Duration 26 | } 27 | 28 | func (c *fakeClock) since(t time.Time) time.Duration { 29 | return c.elapsed 30 | } 31 | 32 | func TestMain(m *testing.M) { 33 | flag.Parse() 34 | setup() 35 | code := m.Run() 36 | teardown() 37 | os.Exit(code) 38 | } 39 | 40 | func setup() { 41 | tp := NewMemoryCacheTransport() 42 | client := http.Client{Transport: tp} 43 | s.transport = tp 44 | s.client = client 45 | s.done = make(chan struct{}) 46 | 47 | mux := http.NewServeMux() 48 | s.server = httptest.NewServer(mux) 49 | 50 | mux.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 51 | w.Header().Set("Cache-Control", "max-age=3600") 52 | })) 53 | 54 | mux.HandleFunc("/method", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | w.Header().Set("Cache-Control", "max-age=3600") 56 | w.Write([]byte(r.Method)) 57 | })) 58 | 59 | mux.HandleFunc("/range", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 60 | lm := "Fri, 14 Dec 2010 01:01:50 GMT" 61 | if r.Header.Get("if-modified-since") == lm { 62 | w.WriteHeader(http.StatusNotModified) 63 | return 64 | } 65 | w.Header().Set("last-modified", lm) 66 | if r.Header.Get("range") == "bytes=4-9" { 67 | w.WriteHeader(http.StatusPartialContent) 68 | w.Write([]byte(" text ")) 69 | return 70 | } 71 | w.Write([]byte("Some text content")) 72 | })) 73 | 74 | mux.HandleFunc("/nostore", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 75 | w.Header().Set("Cache-Control", "no-store") 76 | })) 77 | 78 | mux.HandleFunc("/etag", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 | etag := "124567" 80 | if r.Header.Get("if-none-match") == etag { 81 | w.WriteHeader(http.StatusNotModified) 82 | return 83 | } 84 | w.Header().Set("etag", etag) 85 | })) 86 | 87 | mux.HandleFunc("/lastmodified", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 88 | lm := "Fri, 14 Dec 2010 01:01:50 GMT" 89 | if r.Header.Get("if-modified-since") == lm { 90 | w.WriteHeader(http.StatusNotModified) 91 | return 92 | } 93 | w.Header().Set("last-modified", lm) 94 | })) 95 | 96 | mux.HandleFunc("/varyaccept", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 | w.Header().Set("Cache-Control", "max-age=3600") 98 | w.Header().Set("Content-Type", "text/plain") 99 | w.Header().Set("Vary", "Accept") 100 | w.Write([]byte("Some text content")) 101 | })) 102 | 103 | mux.HandleFunc("/doublevary", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 104 | w.Header().Set("Cache-Control", "max-age=3600") 105 | w.Header().Set("Content-Type", "text/plain") 106 | w.Header().Set("Vary", "Accept, Accept-Language") 107 | w.Write([]byte("Some text content")) 108 | })) 109 | mux.HandleFunc("/2varyheaders", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 110 | w.Header().Set("Cache-Control", "max-age=3600") 111 | w.Header().Set("Content-Type", "text/plain") 112 | w.Header().Add("Vary", "Accept") 113 | w.Header().Add("Vary", "Accept-Language") 114 | w.Write([]byte("Some text content")) 115 | })) 116 | mux.HandleFunc("/varyunused", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 117 | w.Header().Set("Cache-Control", "max-age=3600") 118 | w.Header().Set("Content-Type", "text/plain") 119 | w.Header().Set("Vary", "X-Madeup-Header") 120 | w.Write([]byte("Some text content")) 121 | })) 122 | 123 | mux.HandleFunc("/cachederror", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 124 | etag := "abc" 125 | if r.Header.Get("if-none-match") == etag { 126 | w.WriteHeader(http.StatusNotModified) 127 | return 128 | } 129 | w.Header().Set("etag", etag) 130 | w.WriteHeader(http.StatusNotFound) 131 | w.Write([]byte("Not found")) 132 | })) 133 | 134 | updateFieldsCounter := 0 135 | mux.HandleFunc("/updatefields", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 136 | w.Header().Set("X-Counter", strconv.Itoa(updateFieldsCounter)) 137 | w.Header().Set("Etag", `"e"`) 138 | updateFieldsCounter++ 139 | if r.Header.Get("if-none-match") != "" { 140 | w.WriteHeader(http.StatusNotModified) 141 | return 142 | } 143 | w.Write([]byte("Some text content")) 144 | })) 145 | 146 | // Take 3 seconds to return 200 OK (for testing client timeouts). 147 | mux.HandleFunc("/3seconds", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 148 | time.Sleep(3 * time.Second) 149 | })) 150 | 151 | mux.HandleFunc("/infinite", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 152 | for { 153 | select { 154 | case <-s.done: 155 | return 156 | default: 157 | w.Write([]byte{0}) 158 | } 159 | } 160 | })) 161 | } 162 | 163 | func teardown() { 164 | close(s.done) 165 | s.server.Close() 166 | } 167 | 168 | func resetTest() { 169 | s.transport.Cache = NewMemoryCache() 170 | clock = &realClock{} 171 | } 172 | 173 | // TestCacheableMethod ensures that uncacheable method does not get stored 174 | // in cache and get incorrectly used for a following cacheable method request. 175 | func TestCacheableMethod(t *testing.T) { 176 | resetTest() 177 | { 178 | req, err := http.NewRequest("POST", s.server.URL+"/method", nil) 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | resp, err := s.client.Do(req) 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | var buf bytes.Buffer 187 | _, err = io.Copy(&buf, resp.Body) 188 | if err != nil { 189 | t.Fatal(err) 190 | } 191 | err = resp.Body.Close() 192 | if err != nil { 193 | t.Fatal(err) 194 | } 195 | if got, want := buf.String(), "POST"; got != want { 196 | t.Errorf("got %q, want %q", got, want) 197 | } 198 | if resp.StatusCode != http.StatusOK { 199 | t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) 200 | } 201 | } 202 | { 203 | req, err := http.NewRequest("GET", s.server.URL+"/method", nil) 204 | if err != nil { 205 | t.Fatal(err) 206 | } 207 | resp, err := s.client.Do(req) 208 | if err != nil { 209 | t.Fatal(err) 210 | } 211 | var buf bytes.Buffer 212 | _, err = io.Copy(&buf, resp.Body) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | err = resp.Body.Close() 217 | if err != nil { 218 | t.Fatal(err) 219 | } 220 | if got, want := buf.String(), "GET"; got != want { 221 | t.Errorf("got wrong body %q, want %q", got, want) 222 | } 223 | if resp.StatusCode != http.StatusOK { 224 | t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) 225 | } 226 | if resp.Header.Get(XFromCache) != "" { 227 | t.Errorf("XFromCache header isn't blank") 228 | } 229 | } 230 | } 231 | 232 | func TestDontServeHeadResponseToGetRequest(t *testing.T) { 233 | resetTest() 234 | url := s.server.URL + "/" 235 | req, err := http.NewRequest(http.MethodHead, url, nil) 236 | if err != nil { 237 | t.Fatal(err) 238 | } 239 | _, err = s.client.Do(req) 240 | if err != nil { 241 | t.Fatal(err) 242 | } 243 | req, err = http.NewRequest(http.MethodGet, url, nil) 244 | if err != nil { 245 | t.Fatal(err) 246 | } 247 | resp, err := s.client.Do(req) 248 | if err != nil { 249 | t.Fatal(err) 250 | } 251 | if resp.Header.Get(XFromCache) != "" { 252 | t.Errorf("Cache should not match") 253 | } 254 | } 255 | 256 | func TestDontStorePartialRangeInCache(t *testing.T) { 257 | resetTest() 258 | { 259 | req, err := http.NewRequest("GET", s.server.URL+"/range", nil) 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | req.Header.Set("range", "bytes=4-9") 264 | resp, err := s.client.Do(req) 265 | if err != nil { 266 | t.Fatal(err) 267 | } 268 | var buf bytes.Buffer 269 | _, err = io.Copy(&buf, resp.Body) 270 | if err != nil { 271 | t.Fatal(err) 272 | } 273 | err = resp.Body.Close() 274 | if err != nil { 275 | t.Fatal(err) 276 | } 277 | if got, want := buf.String(), " text "; got != want { 278 | t.Errorf("got %q, want %q", got, want) 279 | } 280 | if resp.StatusCode != http.StatusPartialContent { 281 | t.Errorf("response status code isn't 206 Partial Content: %v", resp.StatusCode) 282 | } 283 | } 284 | { 285 | req, err := http.NewRequest("GET", s.server.URL+"/range", nil) 286 | if err != nil { 287 | t.Fatal(err) 288 | } 289 | resp, err := s.client.Do(req) 290 | if err != nil { 291 | t.Fatal(err) 292 | } 293 | var buf bytes.Buffer 294 | _, err = io.Copy(&buf, resp.Body) 295 | if err != nil { 296 | t.Fatal(err) 297 | } 298 | err = resp.Body.Close() 299 | if err != nil { 300 | t.Fatal(err) 301 | } 302 | if got, want := buf.String(), "Some text content"; got != want { 303 | t.Errorf("got %q, want %q", got, want) 304 | } 305 | if resp.StatusCode != http.StatusOK { 306 | t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) 307 | } 308 | if resp.Header.Get(XFromCache) != "" { 309 | t.Error("XFromCache header isn't blank") 310 | } 311 | } 312 | { 313 | req, err := http.NewRequest("GET", s.server.URL+"/range", nil) 314 | if err != nil { 315 | t.Fatal(err) 316 | } 317 | resp, err := s.client.Do(req) 318 | if err != nil { 319 | t.Fatal(err) 320 | } 321 | var buf bytes.Buffer 322 | _, err = io.Copy(&buf, resp.Body) 323 | if err != nil { 324 | t.Fatal(err) 325 | } 326 | err = resp.Body.Close() 327 | if err != nil { 328 | t.Fatal(err) 329 | } 330 | if got, want := buf.String(), "Some text content"; got != want { 331 | t.Errorf("got %q, want %q", got, want) 332 | } 333 | if resp.StatusCode != http.StatusOK { 334 | t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) 335 | } 336 | if resp.Header.Get(XFromCache) != "1" { 337 | t.Errorf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 338 | } 339 | } 340 | { 341 | req, err := http.NewRequest("GET", s.server.URL+"/range", nil) 342 | if err != nil { 343 | t.Fatal(err) 344 | } 345 | req.Header.Set("range", "bytes=4-9") 346 | resp, err := s.client.Do(req) 347 | if err != nil { 348 | t.Fatal(err) 349 | } 350 | var buf bytes.Buffer 351 | _, err = io.Copy(&buf, resp.Body) 352 | if err != nil { 353 | t.Fatal(err) 354 | } 355 | err = resp.Body.Close() 356 | if err != nil { 357 | t.Fatal(err) 358 | } 359 | if got, want := buf.String(), " text "; got != want { 360 | t.Errorf("got %q, want %q", got, want) 361 | } 362 | if resp.StatusCode != http.StatusPartialContent { 363 | t.Errorf("response status code isn't 206 Partial Content: %v", resp.StatusCode) 364 | } 365 | } 366 | } 367 | 368 | func TestCacheOnlyIfBodyRead(t *testing.T) { 369 | resetTest() 370 | { 371 | req, err := http.NewRequest("GET", s.server.URL, nil) 372 | if err != nil { 373 | t.Fatal(err) 374 | } 375 | resp, err := s.client.Do(req) 376 | if err != nil { 377 | t.Fatal(err) 378 | } 379 | if resp.Header.Get(XFromCache) != "" { 380 | t.Fatal("XFromCache header isn't blank") 381 | } 382 | // We do not read the body 383 | resp.Body.Close() 384 | } 385 | { 386 | req, err := http.NewRequest("GET", s.server.URL, nil) 387 | if err != nil { 388 | t.Fatal(err) 389 | } 390 | resp, err := s.client.Do(req) 391 | if err != nil { 392 | t.Fatal(err) 393 | } 394 | defer resp.Body.Close() 395 | if resp.Header.Get(XFromCache) != "" { 396 | t.Fatalf("XFromCache header isn't blank") 397 | } 398 | } 399 | } 400 | 401 | func TestOnlyReadBodyOnDemand(t *testing.T) { 402 | resetTest() 403 | 404 | req, err := http.NewRequest("GET", s.server.URL+"/infinite", nil) 405 | if err != nil { 406 | t.Fatal(err) 407 | } 408 | resp, err := s.client.Do(req) // This shouldn't hang forever. 409 | if err != nil { 410 | t.Fatal(err) 411 | } 412 | buf := make([]byte, 10) // Only partially read the body. 413 | _, err = resp.Body.Read(buf) 414 | if err != nil { 415 | t.Fatal(err) 416 | } 417 | resp.Body.Close() 418 | } 419 | 420 | func TestGetOnlyIfCachedHit(t *testing.T) { 421 | resetTest() 422 | { 423 | req, err := http.NewRequest("GET", s.server.URL, nil) 424 | if err != nil { 425 | t.Fatal(err) 426 | } 427 | resp, err := s.client.Do(req) 428 | if err != nil { 429 | t.Fatal(err) 430 | } 431 | defer resp.Body.Close() 432 | if resp.Header.Get(XFromCache) != "" { 433 | t.Fatal("XFromCache header isn't blank") 434 | } 435 | _, err = ioutil.ReadAll(resp.Body) 436 | if err != nil { 437 | t.Fatal(err) 438 | } 439 | } 440 | { 441 | req, err := http.NewRequest("GET", s.server.URL, nil) 442 | if err != nil { 443 | t.Fatal(err) 444 | } 445 | req.Header.Add("cache-control", "only-if-cached") 446 | resp, err := s.client.Do(req) 447 | if err != nil { 448 | t.Fatal(err) 449 | } 450 | defer resp.Body.Close() 451 | if resp.Header.Get(XFromCache) != "1" { 452 | t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 453 | } 454 | if resp.StatusCode != http.StatusOK { 455 | t.Fatalf("response status code isn't 200 OK: %v", resp.StatusCode) 456 | } 457 | } 458 | } 459 | 460 | func TestGetOnlyIfCachedMiss(t *testing.T) { 461 | resetTest() 462 | req, err := http.NewRequest("GET", s.server.URL, nil) 463 | if err != nil { 464 | t.Fatal(err) 465 | } 466 | req.Header.Add("cache-control", "only-if-cached") 467 | resp, err := s.client.Do(req) 468 | if err != nil { 469 | t.Fatal(err) 470 | } 471 | defer resp.Body.Close() 472 | if resp.Header.Get(XFromCache) != "" { 473 | t.Fatal("XFromCache header isn't blank") 474 | } 475 | if resp.StatusCode != http.StatusGatewayTimeout { 476 | t.Fatalf("response status code isn't 504 GatewayTimeout: %v", resp.StatusCode) 477 | } 478 | } 479 | 480 | func TestGetNoStoreRequest(t *testing.T) { 481 | resetTest() 482 | req, err := http.NewRequest("GET", s.server.URL, nil) 483 | if err != nil { 484 | t.Fatal(err) 485 | } 486 | req.Header.Add("Cache-Control", "no-store") 487 | { 488 | resp, err := s.client.Do(req) 489 | if err != nil { 490 | t.Fatal(err) 491 | } 492 | defer resp.Body.Close() 493 | if resp.Header.Get(XFromCache) != "" { 494 | t.Fatal("XFromCache header isn't blank") 495 | } 496 | } 497 | { 498 | resp, err := s.client.Do(req) 499 | if err != nil { 500 | t.Fatal(err) 501 | } 502 | defer resp.Body.Close() 503 | if resp.Header.Get(XFromCache) != "" { 504 | t.Fatal("XFromCache header isn't blank") 505 | } 506 | } 507 | } 508 | 509 | func TestGetNoStoreResponse(t *testing.T) { 510 | resetTest() 511 | req, err := http.NewRequest("GET", s.server.URL+"/nostore", nil) 512 | if err != nil { 513 | t.Fatal(err) 514 | } 515 | { 516 | resp, err := s.client.Do(req) 517 | if err != nil { 518 | t.Fatal(err) 519 | } 520 | defer resp.Body.Close() 521 | if resp.Header.Get(XFromCache) != "" { 522 | t.Fatal("XFromCache header isn't blank") 523 | } 524 | } 525 | { 526 | resp, err := s.client.Do(req) 527 | if err != nil { 528 | t.Fatal(err) 529 | } 530 | defer resp.Body.Close() 531 | if resp.Header.Get(XFromCache) != "" { 532 | t.Fatal("XFromCache header isn't blank") 533 | } 534 | } 535 | } 536 | 537 | func TestGetWithEtag(t *testing.T) { 538 | resetTest() 539 | req, err := http.NewRequest("GET", s.server.URL+"/etag", nil) 540 | if err != nil { 541 | t.Fatal(err) 542 | } 543 | { 544 | resp, err := s.client.Do(req) 545 | if err != nil { 546 | t.Fatal(err) 547 | } 548 | defer resp.Body.Close() 549 | if resp.Header.Get(XFromCache) != "" { 550 | t.Fatal("XFromCache header isn't blank") 551 | } 552 | _, err = ioutil.ReadAll(resp.Body) 553 | if err != nil { 554 | t.Fatal(err) 555 | } 556 | 557 | } 558 | { 559 | resp, err := s.client.Do(req) 560 | if err != nil { 561 | t.Fatal(err) 562 | } 563 | defer resp.Body.Close() 564 | if resp.Header.Get(XFromCache) != "1" { 565 | t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 566 | } 567 | // additional assertions to verify that 304 response is converted properly 568 | if resp.StatusCode != http.StatusOK { 569 | t.Fatalf("response status code isn't 200 OK: %v", resp.StatusCode) 570 | } 571 | if _, ok := resp.Header["Connection"]; ok { 572 | t.Fatalf("Connection header isn't absent") 573 | } 574 | } 575 | } 576 | 577 | func TestGetWithLastModified(t *testing.T) { 578 | resetTest() 579 | req, err := http.NewRequest("GET", s.server.URL+"/lastmodified", nil) 580 | if err != nil { 581 | t.Fatal(err) 582 | } 583 | { 584 | resp, err := s.client.Do(req) 585 | if err != nil { 586 | t.Fatal(err) 587 | } 588 | defer resp.Body.Close() 589 | if resp.Header.Get(XFromCache) != "" { 590 | t.Fatal("XFromCache header isn't blank") 591 | } 592 | _, err = ioutil.ReadAll(resp.Body) 593 | if err != nil { 594 | t.Fatal(err) 595 | } 596 | } 597 | { 598 | resp, err := s.client.Do(req) 599 | if err != nil { 600 | t.Fatal(err) 601 | } 602 | defer resp.Body.Close() 603 | if resp.Header.Get(XFromCache) != "1" { 604 | t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 605 | } 606 | } 607 | } 608 | 609 | func TestGetWithVary(t *testing.T) { 610 | resetTest() 611 | req, err := http.NewRequest("GET", s.server.URL+"/varyaccept", nil) 612 | if err != nil { 613 | t.Fatal(err) 614 | } 615 | req.Header.Set("Accept", "text/plain") 616 | { 617 | resp, err := s.client.Do(req) 618 | if err != nil { 619 | t.Fatal(err) 620 | } 621 | defer resp.Body.Close() 622 | if resp.Header.Get("Vary") != "Accept" { 623 | t.Fatalf(`Vary header isn't "Accept": %v`, resp.Header.Get("Vary")) 624 | } 625 | _, err = ioutil.ReadAll(resp.Body) 626 | if err != nil { 627 | t.Fatal(err) 628 | } 629 | } 630 | { 631 | resp, err := s.client.Do(req) 632 | if err != nil { 633 | t.Fatal(err) 634 | } 635 | defer resp.Body.Close() 636 | if resp.Header.Get(XFromCache) != "1" { 637 | t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 638 | } 639 | } 640 | req.Header.Set("Accept", "text/html") 641 | { 642 | resp, err := s.client.Do(req) 643 | if err != nil { 644 | t.Fatal(err) 645 | } 646 | defer resp.Body.Close() 647 | if resp.Header.Get(XFromCache) != "" { 648 | t.Fatal("XFromCache header isn't blank") 649 | } 650 | } 651 | req.Header.Set("Accept", "") 652 | { 653 | resp, err := s.client.Do(req) 654 | if err != nil { 655 | t.Fatal(err) 656 | } 657 | defer resp.Body.Close() 658 | if resp.Header.Get(XFromCache) != "" { 659 | t.Fatal("XFromCache header isn't blank") 660 | } 661 | } 662 | } 663 | 664 | func TestGetWithDoubleVary(t *testing.T) { 665 | resetTest() 666 | req, err := http.NewRequest("GET", s.server.URL+"/doublevary", nil) 667 | if err != nil { 668 | t.Fatal(err) 669 | } 670 | req.Header.Set("Accept", "text/plain") 671 | req.Header.Set("Accept-Language", "da, en-gb;q=0.8, en;q=0.7") 672 | { 673 | resp, err := s.client.Do(req) 674 | if err != nil { 675 | t.Fatal(err) 676 | } 677 | defer resp.Body.Close() 678 | if resp.Header.Get("Vary") == "" { 679 | t.Fatalf(`Vary header is blank`) 680 | } 681 | _, err = ioutil.ReadAll(resp.Body) 682 | if err != nil { 683 | t.Fatal(err) 684 | } 685 | } 686 | { 687 | resp, err := s.client.Do(req) 688 | if err != nil { 689 | t.Fatal(err) 690 | } 691 | defer resp.Body.Close() 692 | if resp.Header.Get(XFromCache) != "1" { 693 | t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 694 | } 695 | } 696 | req.Header.Set("Accept-Language", "") 697 | { 698 | resp, err := s.client.Do(req) 699 | if err != nil { 700 | t.Fatal(err) 701 | } 702 | defer resp.Body.Close() 703 | if resp.Header.Get(XFromCache) != "" { 704 | t.Fatal("XFromCache header isn't blank") 705 | } 706 | } 707 | req.Header.Set("Accept-Language", "da") 708 | { 709 | resp, err := s.client.Do(req) 710 | if err != nil { 711 | t.Fatal(err) 712 | } 713 | defer resp.Body.Close() 714 | if resp.Header.Get(XFromCache) != "" { 715 | t.Fatal("XFromCache header isn't blank") 716 | } 717 | } 718 | } 719 | 720 | func TestGetWith2VaryHeaders(t *testing.T) { 721 | resetTest() 722 | // Tests that multiple Vary headers' comma-separated lists are 723 | // merged. See https://github.com/gregjones/httpcache/issues/27. 724 | const ( 725 | accept = "text/plain" 726 | acceptLanguage = "da, en-gb;q=0.8, en;q=0.7" 727 | ) 728 | req, err := http.NewRequest("GET", s.server.URL+"/2varyheaders", nil) 729 | if err != nil { 730 | t.Fatal(err) 731 | } 732 | req.Header.Set("Accept", accept) 733 | req.Header.Set("Accept-Language", acceptLanguage) 734 | { 735 | resp, err := s.client.Do(req) 736 | if err != nil { 737 | t.Fatal(err) 738 | } 739 | defer resp.Body.Close() 740 | if resp.Header.Get("Vary") == "" { 741 | t.Fatalf(`Vary header is blank`) 742 | } 743 | _, err = ioutil.ReadAll(resp.Body) 744 | if err != nil { 745 | t.Fatal(err) 746 | } 747 | } 748 | { 749 | resp, err := s.client.Do(req) 750 | if err != nil { 751 | t.Fatal(err) 752 | } 753 | defer resp.Body.Close() 754 | if resp.Header.Get(XFromCache) != "1" { 755 | t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 756 | } 757 | } 758 | req.Header.Set("Accept-Language", "") 759 | { 760 | resp, err := s.client.Do(req) 761 | if err != nil { 762 | t.Fatal(err) 763 | } 764 | defer resp.Body.Close() 765 | if resp.Header.Get(XFromCache) != "" { 766 | t.Fatal("XFromCache header isn't blank") 767 | } 768 | } 769 | req.Header.Set("Accept-Language", "da") 770 | { 771 | resp, err := s.client.Do(req) 772 | if err != nil { 773 | t.Fatal(err) 774 | } 775 | defer resp.Body.Close() 776 | if resp.Header.Get(XFromCache) != "" { 777 | t.Fatal("XFromCache header isn't blank") 778 | } 779 | } 780 | req.Header.Set("Accept-Language", acceptLanguage) 781 | req.Header.Set("Accept", "") 782 | { 783 | resp, err := s.client.Do(req) 784 | if err != nil { 785 | t.Fatal(err) 786 | } 787 | defer resp.Body.Close() 788 | if resp.Header.Get(XFromCache) != "" { 789 | t.Fatal("XFromCache header isn't blank") 790 | } 791 | } 792 | req.Header.Set("Accept", "image/png") 793 | { 794 | resp, err := s.client.Do(req) 795 | if err != nil { 796 | t.Fatal(err) 797 | } 798 | defer resp.Body.Close() 799 | if resp.Header.Get(XFromCache) != "" { 800 | t.Fatal("XFromCache header isn't blank") 801 | } 802 | _, err = ioutil.ReadAll(resp.Body) 803 | if err != nil { 804 | t.Fatal(err) 805 | } 806 | } 807 | { 808 | resp, err := s.client.Do(req) 809 | if err != nil { 810 | t.Fatal(err) 811 | } 812 | defer resp.Body.Close() 813 | if resp.Header.Get(XFromCache) != "1" { 814 | t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 815 | } 816 | } 817 | } 818 | 819 | func TestGetVaryUnused(t *testing.T) { 820 | resetTest() 821 | req, err := http.NewRequest("GET", s.server.URL+"/varyunused", nil) 822 | if err != nil { 823 | t.Fatal(err) 824 | } 825 | req.Header.Set("Accept", "text/plain") 826 | { 827 | resp, err := s.client.Do(req) 828 | if err != nil { 829 | t.Fatal(err) 830 | } 831 | defer resp.Body.Close() 832 | if resp.Header.Get("Vary") == "" { 833 | t.Fatalf(`Vary header is blank`) 834 | } 835 | _, err = ioutil.ReadAll(resp.Body) 836 | if err != nil { 837 | t.Fatal(err) 838 | } 839 | } 840 | { 841 | resp, err := s.client.Do(req) 842 | if err != nil { 843 | t.Fatal(err) 844 | } 845 | defer resp.Body.Close() 846 | if resp.Header.Get(XFromCache) != "1" { 847 | t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 848 | } 849 | } 850 | } 851 | 852 | func TestUpdateFields(t *testing.T) { 853 | resetTest() 854 | req, err := http.NewRequest("GET", s.server.URL+"/updatefields", nil) 855 | if err != nil { 856 | t.Fatal(err) 857 | } 858 | var counter, counter2 string 859 | { 860 | resp, err := s.client.Do(req) 861 | if err != nil { 862 | t.Fatal(err) 863 | } 864 | defer resp.Body.Close() 865 | counter = resp.Header.Get("x-counter") 866 | _, err = ioutil.ReadAll(resp.Body) 867 | if err != nil { 868 | t.Fatal(err) 869 | } 870 | } 871 | { 872 | resp, err := s.client.Do(req) 873 | if err != nil { 874 | t.Fatal(err) 875 | } 876 | defer resp.Body.Close() 877 | if resp.Header.Get(XFromCache) != "1" { 878 | t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) 879 | } 880 | counter2 = resp.Header.Get("x-counter") 881 | } 882 | if counter == counter2 { 883 | t.Fatalf(`both "x-counter" values are equal: %v %v`, counter, counter2) 884 | } 885 | } 886 | 887 | // This tests the fix for https://github.com/gregjones/httpcache/issues/74. 888 | // Previously, after validating a cached response, its StatusCode 889 | // was incorrectly being replaced. 890 | func TestCachedErrorsKeepStatus(t *testing.T) { 891 | resetTest() 892 | req, err := http.NewRequest("GET", s.server.URL+"/cachederror", nil) 893 | if err != nil { 894 | t.Fatal(err) 895 | } 896 | { 897 | resp, err := s.client.Do(req) 898 | if err != nil { 899 | t.Fatal(err) 900 | } 901 | defer resp.Body.Close() 902 | io.Copy(ioutil.Discard, resp.Body) 903 | } 904 | { 905 | resp, err := s.client.Do(req) 906 | if err != nil { 907 | t.Fatal(err) 908 | } 909 | defer resp.Body.Close() 910 | if resp.StatusCode != http.StatusNotFound { 911 | t.Fatalf("Status code isn't 404: %d", resp.StatusCode) 912 | } 913 | } 914 | } 915 | 916 | func TestParseCacheControl(t *testing.T) { 917 | resetTest() 918 | h := http.Header{} 919 | for range parseCacheControl(h) { 920 | t.Fatal("cacheControl should be empty") 921 | } 922 | 923 | h.Set("cache-control", "no-cache") 924 | { 925 | cc := parseCacheControl(h) 926 | if _, ok := cc["foo"]; ok { 927 | t.Error(`Value "foo" shouldn't exist`) 928 | } 929 | noCache, ok := cc["no-cache"] 930 | if !ok { 931 | t.Fatalf(`"no-cache" value isn't set`) 932 | } 933 | if noCache != "" { 934 | t.Fatalf(`"no-cache" value isn't blank: %v`, noCache) 935 | } 936 | } 937 | h.Set("cache-control", "no-cache, max-age=3600") 938 | { 939 | cc := parseCacheControl(h) 940 | noCache, ok := cc["no-cache"] 941 | if !ok { 942 | t.Fatalf(`"no-cache" value isn't set`) 943 | } 944 | if noCache != "" { 945 | t.Fatalf(`"no-cache" value isn't blank: %v`, noCache) 946 | } 947 | if cc["max-age"] != "3600" { 948 | t.Fatalf(`"max-age" value isn't "3600": %v`, cc["max-age"]) 949 | } 950 | } 951 | } 952 | 953 | func TestNoCacheRequestExpiration(t *testing.T) { 954 | resetTest() 955 | respHeaders := http.Header{} 956 | respHeaders.Set("Cache-Control", "max-age=7200") 957 | 958 | reqHeaders := http.Header{} 959 | reqHeaders.Set("Cache-Control", "no-cache") 960 | if getFreshness(respHeaders, reqHeaders) != transparent { 961 | t.Fatal("freshness isn't transparent") 962 | } 963 | } 964 | 965 | func TestNoCacheResponseExpiration(t *testing.T) { 966 | resetTest() 967 | respHeaders := http.Header{} 968 | respHeaders.Set("Cache-Control", "no-cache") 969 | respHeaders.Set("Expires", "Wed, 19 Apr 3000 11:43:00 GMT") 970 | 971 | reqHeaders := http.Header{} 972 | if getFreshness(respHeaders, reqHeaders) != stale { 973 | t.Fatal("freshness isn't stale") 974 | } 975 | } 976 | 977 | func TestReqMustRevalidate(t *testing.T) { 978 | resetTest() 979 | // not paying attention to request setting max-stale means never returning stale 980 | // responses, so always acting as if must-revalidate is set 981 | respHeaders := http.Header{} 982 | 983 | reqHeaders := http.Header{} 984 | reqHeaders.Set("Cache-Control", "must-revalidate") 985 | if getFreshness(respHeaders, reqHeaders) != stale { 986 | t.Fatal("freshness isn't stale") 987 | } 988 | } 989 | 990 | func TestRespMustRevalidate(t *testing.T) { 991 | resetTest() 992 | respHeaders := http.Header{} 993 | respHeaders.Set("Cache-Control", "must-revalidate") 994 | 995 | reqHeaders := http.Header{} 996 | if getFreshness(respHeaders, reqHeaders) != stale { 997 | t.Fatal("freshness isn't stale") 998 | } 999 | } 1000 | 1001 | func TestFreshExpiration(t *testing.T) { 1002 | resetTest() 1003 | now := time.Now() 1004 | respHeaders := http.Header{} 1005 | respHeaders.Set("date", now.Format(time.RFC1123)) 1006 | respHeaders.Set("expires", now.Add(time.Duration(2)*time.Second).Format(time.RFC1123)) 1007 | 1008 | reqHeaders := http.Header{} 1009 | if getFreshness(respHeaders, reqHeaders) != fresh { 1010 | t.Fatal("freshness isn't fresh") 1011 | } 1012 | 1013 | clock = &fakeClock{elapsed: 3 * time.Second} 1014 | if getFreshness(respHeaders, reqHeaders) != stale { 1015 | t.Fatal("freshness isn't stale") 1016 | } 1017 | } 1018 | 1019 | func TestMaxAge(t *testing.T) { 1020 | resetTest() 1021 | now := time.Now() 1022 | respHeaders := http.Header{} 1023 | respHeaders.Set("date", now.Format(time.RFC1123)) 1024 | respHeaders.Set("cache-control", "max-age=2") 1025 | 1026 | reqHeaders := http.Header{} 1027 | if getFreshness(respHeaders, reqHeaders) != fresh { 1028 | t.Fatal("freshness isn't fresh") 1029 | } 1030 | 1031 | clock = &fakeClock{elapsed: 3 * time.Second} 1032 | if getFreshness(respHeaders, reqHeaders) != stale { 1033 | t.Fatal("freshness isn't stale") 1034 | } 1035 | } 1036 | 1037 | func TestMaxAgeZero(t *testing.T) { 1038 | resetTest() 1039 | now := time.Now() 1040 | respHeaders := http.Header{} 1041 | respHeaders.Set("date", now.Format(time.RFC1123)) 1042 | respHeaders.Set("cache-control", "max-age=0") 1043 | 1044 | reqHeaders := http.Header{} 1045 | if getFreshness(respHeaders, reqHeaders) != stale { 1046 | t.Fatal("freshness isn't stale") 1047 | } 1048 | } 1049 | 1050 | func TestBothMaxAge(t *testing.T) { 1051 | resetTest() 1052 | now := time.Now() 1053 | respHeaders := http.Header{} 1054 | respHeaders.Set("date", now.Format(time.RFC1123)) 1055 | respHeaders.Set("cache-control", "max-age=2") 1056 | 1057 | reqHeaders := http.Header{} 1058 | reqHeaders.Set("cache-control", "max-age=0") 1059 | if getFreshness(respHeaders, reqHeaders) != stale { 1060 | t.Fatal("freshness isn't stale") 1061 | } 1062 | } 1063 | 1064 | func TestMinFreshWithExpires(t *testing.T) { 1065 | resetTest() 1066 | now := time.Now() 1067 | respHeaders := http.Header{} 1068 | respHeaders.Set("date", now.Format(time.RFC1123)) 1069 | respHeaders.Set("expires", now.Add(time.Duration(2)*time.Second).Format(time.RFC1123)) 1070 | 1071 | reqHeaders := http.Header{} 1072 | reqHeaders.Set("cache-control", "min-fresh=1") 1073 | if getFreshness(respHeaders, reqHeaders) != fresh { 1074 | t.Fatal("freshness isn't fresh") 1075 | } 1076 | 1077 | reqHeaders = http.Header{} 1078 | reqHeaders.Set("cache-control", "min-fresh=2") 1079 | if getFreshness(respHeaders, reqHeaders) != stale { 1080 | t.Fatal("freshness isn't stale") 1081 | } 1082 | } 1083 | 1084 | func TestEmptyMaxStale(t *testing.T) { 1085 | resetTest() 1086 | now := time.Now() 1087 | respHeaders := http.Header{} 1088 | respHeaders.Set("date", now.Format(time.RFC1123)) 1089 | respHeaders.Set("cache-control", "max-age=20") 1090 | 1091 | reqHeaders := http.Header{} 1092 | reqHeaders.Set("cache-control", "max-stale") 1093 | clock = &fakeClock{elapsed: 10 * time.Second} 1094 | if getFreshness(respHeaders, reqHeaders) != fresh { 1095 | t.Fatal("freshness isn't fresh") 1096 | } 1097 | 1098 | clock = &fakeClock{elapsed: 60 * time.Second} 1099 | if getFreshness(respHeaders, reqHeaders) != fresh { 1100 | t.Fatal("freshness isn't fresh") 1101 | } 1102 | } 1103 | 1104 | func TestMaxStaleValue(t *testing.T) { 1105 | resetTest() 1106 | now := time.Now() 1107 | respHeaders := http.Header{} 1108 | respHeaders.Set("date", now.Format(time.RFC1123)) 1109 | respHeaders.Set("cache-control", "max-age=10") 1110 | 1111 | reqHeaders := http.Header{} 1112 | reqHeaders.Set("cache-control", "max-stale=20") 1113 | clock = &fakeClock{elapsed: 5 * time.Second} 1114 | if getFreshness(respHeaders, reqHeaders) != fresh { 1115 | t.Fatal("freshness isn't fresh") 1116 | } 1117 | 1118 | clock = &fakeClock{elapsed: 15 * time.Second} 1119 | if getFreshness(respHeaders, reqHeaders) != fresh { 1120 | t.Fatal("freshness isn't fresh") 1121 | } 1122 | 1123 | clock = &fakeClock{elapsed: 30 * time.Second} 1124 | if getFreshness(respHeaders, reqHeaders) != stale { 1125 | t.Fatal("freshness isn't stale") 1126 | } 1127 | } 1128 | 1129 | func containsHeader(headers []string, header string) bool { 1130 | for _, v := range headers { 1131 | if http.CanonicalHeaderKey(v) == http.CanonicalHeaderKey(header) { 1132 | return true 1133 | } 1134 | } 1135 | return false 1136 | } 1137 | 1138 | func TestGetEndToEndHeaders(t *testing.T) { 1139 | resetTest() 1140 | var ( 1141 | headers http.Header 1142 | end2end []string 1143 | ) 1144 | 1145 | headers = http.Header{} 1146 | headers.Set("content-type", "text/html") 1147 | headers.Set("te", "deflate") 1148 | 1149 | end2end = getEndToEndHeaders(headers) 1150 | if !containsHeader(end2end, "content-type") { 1151 | t.Fatal(`doesn't contain "content-type" header`) 1152 | } 1153 | if containsHeader(end2end, "te") { 1154 | t.Fatal(`doesn't contain "te" header`) 1155 | } 1156 | 1157 | headers = http.Header{} 1158 | headers.Set("connection", "content-type") 1159 | headers.Set("content-type", "text/csv") 1160 | headers.Set("te", "deflate") 1161 | end2end = getEndToEndHeaders(headers) 1162 | if containsHeader(end2end, "connection") { 1163 | t.Fatal(`doesn't contain "connection" header`) 1164 | } 1165 | if containsHeader(end2end, "content-type") { 1166 | t.Fatal(`doesn't contain "content-type" header`) 1167 | } 1168 | if containsHeader(end2end, "te") { 1169 | t.Fatal(`doesn't contain "te" header`) 1170 | } 1171 | 1172 | headers = http.Header{} 1173 | end2end = getEndToEndHeaders(headers) 1174 | if len(end2end) != 0 { 1175 | t.Fatal(`non-zero end2end headers`) 1176 | } 1177 | 1178 | headers = http.Header{} 1179 | headers.Set("connection", "content-type") 1180 | end2end = getEndToEndHeaders(headers) 1181 | if len(end2end) != 0 { 1182 | t.Fatal(`non-zero end2end headers`) 1183 | } 1184 | } 1185 | 1186 | type transportMock struct { 1187 | response *http.Response 1188 | err error 1189 | } 1190 | 1191 | func (t transportMock) RoundTrip(req *http.Request) (resp *http.Response, err error) { 1192 | return t.response, t.err 1193 | } 1194 | 1195 | func TestStaleIfErrorRequest(t *testing.T) { 1196 | resetTest() 1197 | now := time.Now() 1198 | tmock := transportMock{ 1199 | response: &http.Response{ 1200 | Status: http.StatusText(http.StatusOK), 1201 | StatusCode: http.StatusOK, 1202 | Header: http.Header{ 1203 | "Date": []string{now.Format(time.RFC1123)}, 1204 | "Cache-Control": []string{"no-cache"}, 1205 | }, 1206 | Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), 1207 | }, 1208 | err: nil, 1209 | } 1210 | tp := NewMemoryCacheTransport() 1211 | tp.Transport = &tmock 1212 | 1213 | // First time, response is cached on success 1214 | r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) 1215 | r.Header.Set("Cache-Control", "stale-if-error") 1216 | resp, err := tp.RoundTrip(r) 1217 | if err != nil { 1218 | t.Fatal(err) 1219 | } 1220 | if resp == nil { 1221 | t.Fatal("resp is nil") 1222 | } 1223 | _, err = ioutil.ReadAll(resp.Body) 1224 | if err != nil { 1225 | t.Fatal(err) 1226 | } 1227 | 1228 | // On failure, response is returned from the cache 1229 | tmock.response = nil 1230 | tmock.err = errors.New("some error") 1231 | resp, err = tp.RoundTrip(r) 1232 | if err != nil { 1233 | t.Fatal(err) 1234 | } 1235 | if resp == nil { 1236 | t.Fatal("resp is nil") 1237 | } 1238 | } 1239 | 1240 | func TestStaleIfErrorRequestLifetime(t *testing.T) { 1241 | resetTest() 1242 | now := time.Now() 1243 | tmock := transportMock{ 1244 | response: &http.Response{ 1245 | Status: http.StatusText(http.StatusOK), 1246 | StatusCode: http.StatusOK, 1247 | Header: http.Header{ 1248 | "Date": []string{now.Format(time.RFC1123)}, 1249 | "Cache-Control": []string{"no-cache"}, 1250 | }, 1251 | Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), 1252 | }, 1253 | err: nil, 1254 | } 1255 | tp := NewMemoryCacheTransport() 1256 | tp.Transport = &tmock 1257 | 1258 | // First time, response is cached on success 1259 | r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) 1260 | r.Header.Set("Cache-Control", "stale-if-error=100") 1261 | resp, err := tp.RoundTrip(r) 1262 | if err != nil { 1263 | t.Fatal(err) 1264 | } 1265 | if resp == nil { 1266 | t.Fatal("resp is nil") 1267 | } 1268 | _, err = ioutil.ReadAll(resp.Body) 1269 | if err != nil { 1270 | t.Fatal(err) 1271 | } 1272 | 1273 | // On failure, response is returned from the cache 1274 | tmock.response = nil 1275 | tmock.err = errors.New("some error") 1276 | resp, err = tp.RoundTrip(r) 1277 | if err != nil { 1278 | t.Fatal(err) 1279 | } 1280 | if resp == nil { 1281 | t.Fatal("resp is nil") 1282 | } 1283 | 1284 | // Same for http errors 1285 | tmock.response = &http.Response{StatusCode: http.StatusInternalServerError} 1286 | tmock.err = nil 1287 | resp, err = tp.RoundTrip(r) 1288 | if err != nil { 1289 | t.Fatal(err) 1290 | } 1291 | if resp == nil { 1292 | t.Fatal("resp is nil") 1293 | } 1294 | 1295 | // If failure last more than max stale, error is returned 1296 | clock = &fakeClock{elapsed: 200 * time.Second} 1297 | _, err = tp.RoundTrip(r) 1298 | if err != tmock.err { 1299 | t.Fatalf("got err %v, want %v", err, tmock.err) 1300 | } 1301 | } 1302 | 1303 | func TestStaleIfErrorResponse(t *testing.T) { 1304 | resetTest() 1305 | now := time.Now() 1306 | tmock := transportMock{ 1307 | response: &http.Response{ 1308 | Status: http.StatusText(http.StatusOK), 1309 | StatusCode: http.StatusOK, 1310 | Header: http.Header{ 1311 | "Date": []string{now.Format(time.RFC1123)}, 1312 | "Cache-Control": []string{"no-cache, stale-if-error"}, 1313 | }, 1314 | Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), 1315 | }, 1316 | err: nil, 1317 | } 1318 | tp := NewMemoryCacheTransport() 1319 | tp.Transport = &tmock 1320 | 1321 | // First time, response is cached on success 1322 | r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) 1323 | resp, err := tp.RoundTrip(r) 1324 | if err != nil { 1325 | t.Fatal(err) 1326 | } 1327 | if resp == nil { 1328 | t.Fatal("resp is nil") 1329 | } 1330 | _, err = ioutil.ReadAll(resp.Body) 1331 | if err != nil { 1332 | t.Fatal(err) 1333 | } 1334 | 1335 | // On failure, response is returned from the cache 1336 | tmock.response = nil 1337 | tmock.err = errors.New("some error") 1338 | resp, err = tp.RoundTrip(r) 1339 | if err != nil { 1340 | t.Fatal(err) 1341 | } 1342 | if resp == nil { 1343 | t.Fatal("resp is nil") 1344 | } 1345 | } 1346 | 1347 | func TestStaleIfErrorResponseLifetime(t *testing.T) { 1348 | resetTest() 1349 | now := time.Now() 1350 | tmock := transportMock{ 1351 | response: &http.Response{ 1352 | Status: http.StatusText(http.StatusOK), 1353 | StatusCode: http.StatusOK, 1354 | Header: http.Header{ 1355 | "Date": []string{now.Format(time.RFC1123)}, 1356 | "Cache-Control": []string{"no-cache, stale-if-error=100"}, 1357 | }, 1358 | Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), 1359 | }, 1360 | err: nil, 1361 | } 1362 | tp := NewMemoryCacheTransport() 1363 | tp.Transport = &tmock 1364 | 1365 | // First time, response is cached on success 1366 | r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) 1367 | resp, err := tp.RoundTrip(r) 1368 | if err != nil { 1369 | t.Fatal(err) 1370 | } 1371 | if resp == nil { 1372 | t.Fatal("resp is nil") 1373 | } 1374 | _, err = ioutil.ReadAll(resp.Body) 1375 | if err != nil { 1376 | t.Fatal(err) 1377 | } 1378 | 1379 | // On failure, response is returned from the cache 1380 | tmock.response = nil 1381 | tmock.err = errors.New("some error") 1382 | resp, err = tp.RoundTrip(r) 1383 | if err != nil { 1384 | t.Fatal(err) 1385 | } 1386 | if resp == nil { 1387 | t.Fatal("resp is nil") 1388 | } 1389 | 1390 | // If failure last more than max stale, error is returned 1391 | clock = &fakeClock{elapsed: 200 * time.Second} 1392 | _, err = tp.RoundTrip(r) 1393 | if err != tmock.err { 1394 | t.Fatalf("got err %v, want %v", err, tmock.err) 1395 | } 1396 | } 1397 | 1398 | // This tests the fix for https://github.com/gregjones/httpcache/issues/74. 1399 | // Previously, after a stale response was used after encountering an error, 1400 | // its StatusCode was being incorrectly replaced. 1401 | func TestStaleIfErrorKeepsStatus(t *testing.T) { 1402 | resetTest() 1403 | now := time.Now() 1404 | tmock := transportMock{ 1405 | response: &http.Response{ 1406 | Status: http.StatusText(http.StatusNotFound), 1407 | StatusCode: http.StatusNotFound, 1408 | Header: http.Header{ 1409 | "Date": []string{now.Format(time.RFC1123)}, 1410 | "Cache-Control": []string{"no-cache"}, 1411 | }, 1412 | Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), 1413 | }, 1414 | err: nil, 1415 | } 1416 | tp := NewMemoryCacheTransport() 1417 | tp.Transport = &tmock 1418 | 1419 | // First time, response is cached on success 1420 | r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) 1421 | r.Header.Set("Cache-Control", "stale-if-error") 1422 | resp, err := tp.RoundTrip(r) 1423 | if err != nil { 1424 | t.Fatal(err) 1425 | } 1426 | if resp == nil { 1427 | t.Fatal("resp is nil") 1428 | } 1429 | _, err = ioutil.ReadAll(resp.Body) 1430 | if err != nil { 1431 | t.Fatal(err) 1432 | } 1433 | 1434 | // On failure, response is returned from the cache 1435 | tmock.response = nil 1436 | tmock.err = errors.New("some error") 1437 | resp, err = tp.RoundTrip(r) 1438 | if err != nil { 1439 | t.Fatal(err) 1440 | } 1441 | if resp == nil { 1442 | t.Fatal("resp is nil") 1443 | } 1444 | if resp.StatusCode != http.StatusNotFound { 1445 | t.Fatalf("Status wasn't 404: %d", resp.StatusCode) 1446 | } 1447 | } 1448 | 1449 | // Test that http.Client.Timeout is respected when cache transport is used. 1450 | // That is so as long as request cancellation is propagated correctly. 1451 | // In the past, that required CancelRequest to be implemented correctly, 1452 | // but modern http.Client uses Request.Cancel (or request context) instead, 1453 | // so we don't have to do anything. 1454 | func TestClientTimeout(t *testing.T) { 1455 | if testing.Short() { 1456 | t.Skip("skipping timeout test in short mode") // Because it takes at least 3 seconds to run. 1457 | } 1458 | resetTest() 1459 | client := &http.Client{ 1460 | Transport: NewMemoryCacheTransport(), 1461 | Timeout: time.Second, 1462 | } 1463 | started := time.Now() 1464 | resp, err := client.Get(s.server.URL + "/3seconds") 1465 | taken := time.Since(started) 1466 | if err == nil { 1467 | t.Error("got nil error, want timeout error") 1468 | } 1469 | if resp != nil { 1470 | t.Error("got non-nil resp, want nil resp") 1471 | } 1472 | if taken >= 2*time.Second { 1473 | t.Error("client.Do took 2+ seconds, want < 2 seconds") 1474 | } 1475 | } 1476 | -------------------------------------------------------------------------------- /leveldbcache/leveldbcache.go: -------------------------------------------------------------------------------- 1 | // Package leveldbcache provides an implementation of httpcache.Cache that 2 | // uses github.com/syndtr/goleveldb/leveldb 3 | package leveldbcache 4 | 5 | import ( 6 | "github.com/syndtr/goleveldb/leveldb" 7 | ) 8 | 9 | // Cache is an implementation of httpcache.Cache with leveldb storage 10 | type Cache struct { 11 | db *leveldb.DB 12 | } 13 | 14 | // Get returns the response corresponding to key if present 15 | func (c *Cache) Get(key string) (resp []byte, ok bool) { 16 | var err error 17 | resp, err = c.db.Get([]byte(key), nil) 18 | if err != nil { 19 | return []byte{}, false 20 | } 21 | return resp, true 22 | } 23 | 24 | // Set saves a response to the cache as key 25 | func (c *Cache) Set(key string, resp []byte) { 26 | c.db.Put([]byte(key), resp, nil) 27 | } 28 | 29 | // Delete removes the response with key from the cache 30 | func (c *Cache) Delete(key string) { 31 | c.db.Delete([]byte(key), nil) 32 | } 33 | 34 | // New returns a new Cache that will store leveldb in path 35 | func New(path string) (*Cache, error) { 36 | cache := &Cache{} 37 | 38 | var err error 39 | cache.db, err = leveldb.OpenFile(path, nil) 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | return cache, nil 45 | } 46 | 47 | // NewWithDB returns a new Cache using the provided leveldb as underlying 48 | // storage. 49 | func NewWithDB(db *leveldb.DB) *Cache { 50 | return &Cache{db} 51 | } 52 | -------------------------------------------------------------------------------- /leveldbcache/leveldbcache_test.go: -------------------------------------------------------------------------------- 1 | package leveldbcache 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/gregjones/httpcache/test" 10 | ) 11 | 12 | func TestDiskCache(t *testing.T) { 13 | tempDir, err := ioutil.TempDir("", "httpcache") 14 | if err != nil { 15 | t.Fatalf("TempDir: %v", err) 16 | } 17 | defer os.RemoveAll(tempDir) 18 | 19 | cache, err := New(filepath.Join(tempDir, "db")) 20 | if err != nil { 21 | t.Fatalf("New leveldb,: %v", err) 22 | } 23 | 24 | test.Cache(t, cache) 25 | } 26 | -------------------------------------------------------------------------------- /memcache/appengine.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | // Package memcache provides an implementation of httpcache.Cache that uses App 4 | // Engine's memcache package to store cached responses. 5 | // 6 | // When not built for Google App Engine, this package will provide an 7 | // implementation that connects to a specified memcached server. See the 8 | // memcache.go file in this package for details. 9 | package memcache 10 | 11 | import ( 12 | "appengine" 13 | "appengine/memcache" 14 | ) 15 | 16 | // Cache is an implementation of httpcache.Cache that caches responses in App 17 | // Engine's memcache. 18 | type Cache struct { 19 | appengine.Context 20 | } 21 | 22 | // cacheKey modifies an httpcache key for use in memcache. Specifically, it 23 | // prefixes keys to avoid collision with other data stored in memcache. 24 | func cacheKey(key string) string { 25 | return "httpcache:" + key 26 | } 27 | 28 | // Get returns the response corresponding to key if present. 29 | func (c *Cache) Get(key string) (resp []byte, ok bool) { 30 | item, err := memcache.Get(c.Context, cacheKey(key)) 31 | if err != nil { 32 | if err != memcache.ErrCacheMiss { 33 | c.Context.Errorf("error getting cached response: %v", err) 34 | } 35 | return nil, false 36 | } 37 | return item.Value, true 38 | } 39 | 40 | // Set saves a response to the cache as key. 41 | func (c *Cache) Set(key string, resp []byte) { 42 | item := &memcache.Item{ 43 | Key: cacheKey(key), 44 | Value: resp, 45 | } 46 | if err := memcache.Set(c.Context, item); err != nil { 47 | c.Context.Errorf("error caching response: %v", err) 48 | } 49 | } 50 | 51 | // Delete removes the response with key from the cache. 52 | func (c *Cache) Delete(key string) { 53 | if err := memcache.Delete(c.Context, cacheKey(key)); err != nil { 54 | c.Context.Errorf("error deleting cached response: %v", err) 55 | } 56 | } 57 | 58 | // New returns a new Cache for the given context. 59 | func New(ctx appengine.Context) *Cache { 60 | return &Cache{ctx} 61 | } 62 | -------------------------------------------------------------------------------- /memcache/appengine_test.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package memcache 4 | 5 | import ( 6 | "testing" 7 | 8 | "appengine/aetest" 9 | "github.com/gregjones/httpcache/test" 10 | ) 11 | 12 | func TestAppEngine(t *testing.T) { 13 | ctx, err := aetest.NewContext(nil) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer ctx.Close() 18 | 19 | test.Cache(t, New(ctx)) 20 | } 21 | -------------------------------------------------------------------------------- /memcache/memcache.go: -------------------------------------------------------------------------------- 1 | // +build !appengine 2 | 3 | // Package memcache provides an implementation of httpcache.Cache that uses 4 | // gomemcache to store cached responses. 5 | // 6 | // When built for Google App Engine, this package will provide an 7 | // implementation that uses App Engine's memcache service. See the 8 | // appengine.go file in this package for details. 9 | package memcache 10 | 11 | import ( 12 | "github.com/bradfitz/gomemcache/memcache" 13 | ) 14 | 15 | // Cache is an implementation of httpcache.Cache that caches responses in a 16 | // memcache server. 17 | type Cache struct { 18 | *memcache.Client 19 | } 20 | 21 | // cacheKey modifies an httpcache key for use in memcache. Specifically, it 22 | // prefixes keys to avoid collision with other data stored in memcache. 23 | func cacheKey(key string) string { 24 | return "httpcache:" + key 25 | } 26 | 27 | // Get returns the response corresponding to key if present. 28 | func (c *Cache) Get(key string) (resp []byte, ok bool) { 29 | item, err := c.Client.Get(cacheKey(key)) 30 | if err != nil { 31 | return nil, false 32 | } 33 | return item.Value, true 34 | } 35 | 36 | // Set saves a response to the cache as key. 37 | func (c *Cache) Set(key string, resp []byte) { 38 | item := &memcache.Item{ 39 | Key: cacheKey(key), 40 | Value: resp, 41 | } 42 | c.Client.Set(item) 43 | } 44 | 45 | // Delete removes the response with key from the cache. 46 | func (c *Cache) Delete(key string) { 47 | c.Client.Delete(cacheKey(key)) 48 | } 49 | 50 | // New returns a new Cache using the provided memcache server(s) with equal 51 | // weight. If a server is listed multiple times, it gets a proportional amount 52 | // of weight. 53 | func New(server ...string) *Cache { 54 | return NewWithClient(memcache.New(server...)) 55 | } 56 | 57 | // NewWithClient returns a new Cache with the given memcache client. 58 | func NewWithClient(client *memcache.Client) *Cache { 59 | return &Cache{client} 60 | } 61 | -------------------------------------------------------------------------------- /memcache/memcache_test.go: -------------------------------------------------------------------------------- 1 | // +build !appengine 2 | 3 | package memcache 4 | 5 | import ( 6 | "net" 7 | "testing" 8 | 9 | "github.com/gregjones/httpcache/test" 10 | ) 11 | 12 | const testServer = "localhost:11211" 13 | 14 | func TestMemCache(t *testing.T) { 15 | conn, err := net.Dial("tcp", testServer) 16 | if err != nil { 17 | // TODO: rather than skip the test, fall back to a faked memcached server 18 | t.Skipf("skipping test; no server running at %s", testServer) 19 | } 20 | conn.Write([]byte("flush_all\r\n")) // flush memcache 21 | conn.Close() 22 | 23 | test.Cache(t, New(testServer)) 24 | } 25 | -------------------------------------------------------------------------------- /redis/redis.go: -------------------------------------------------------------------------------- 1 | // Package redis provides a redis interface for http caching. 2 | package redis 3 | 4 | import ( 5 | "github.com/gomodule/redigo/redis" 6 | "github.com/gregjones/httpcache" 7 | ) 8 | 9 | // cache is an implementation of httpcache.Cache that caches responses in a 10 | // redis server. 11 | type cache struct { 12 | redis.Conn 13 | } 14 | 15 | // cacheKey modifies an httpcache key for use in redis. Specifically, it 16 | // prefixes keys to avoid collision with other data stored in redis. 17 | func cacheKey(key string) string { 18 | return "rediscache:" + key 19 | } 20 | 21 | // Get returns the response corresponding to key if present. 22 | func (c cache) Get(key string) (resp []byte, ok bool) { 23 | item, err := redis.Bytes(c.Do("GET", cacheKey(key))) 24 | if err != nil { 25 | return nil, false 26 | } 27 | return item, true 28 | } 29 | 30 | // Set saves a response to the cache as key. 31 | func (c cache) Set(key string, resp []byte) { 32 | c.Do("SET", cacheKey(key), resp) 33 | } 34 | 35 | // Delete removes the response with key from the cache. 36 | func (c cache) Delete(key string) { 37 | c.Do("DEL", cacheKey(key)) 38 | } 39 | 40 | // NewWithClient returns a new Cache with the given redis connection. 41 | func NewWithClient(client redis.Conn) httpcache.Cache { 42 | return cache{client} 43 | } 44 | -------------------------------------------------------------------------------- /redis/redis_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gomodule/redigo/redis" 7 | "github.com/gregjones/httpcache/test" 8 | ) 9 | 10 | func TestRedisCache(t *testing.T) { 11 | conn, err := redis.Dial("tcp", "localhost:6379") 12 | if err != nil { 13 | // TODO: rather than skip the test, fall back to a faked redis server 14 | t.Skipf("skipping test; no server running at localhost:6379") 15 | } 16 | conn.Do("FLUSHALL") 17 | 18 | test.Cache(t, NewWithClient(conn)) 19 | } 20 | -------------------------------------------------------------------------------- /test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/gregjones/httpcache" 8 | ) 9 | 10 | // Cache excercises a httpcache.Cache implementation. 11 | func Cache(t *testing.T, cache httpcache.Cache) { 12 | key := "testKey" 13 | _, ok := cache.Get(key) 14 | if ok { 15 | t.Fatal("retrieved key before adding it") 16 | } 17 | 18 | val := []byte("some bytes") 19 | cache.Set(key, val) 20 | 21 | retVal, ok := cache.Get(key) 22 | if !ok { 23 | t.Fatal("could not retrieve an element we just added") 24 | } 25 | if !bytes.Equal(retVal, val) { 26 | t.Fatal("retrieved a different value than what we put in") 27 | } 28 | 29 | cache.Delete(key) 30 | 31 | _, ok = cache.Get(key) 32 | if ok { 33 | t.Fatal("deleted key still present") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/test_test.go: -------------------------------------------------------------------------------- 1 | package test_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gregjones/httpcache" 7 | "github.com/gregjones/httpcache/test" 8 | ) 9 | 10 | func TestMemoryCache(t *testing.T) { 11 | test.Cache(t, httpcache.NewMemoryCache()) 12 | } 13 | --------------------------------------------------------------------------------