blob: 72f38603e00da40de73b90c9d1d6076cd13ad627 [file] [log] [blame]
package rfc
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import (
"fmt"
"net/http"
"testing"
"time"
)
func ExampleParseCacheControl() {
hdrs := http.Header{}
hdrs.Set(CacheControl, "no-store, no-cache, must-revalidate, post-check=0, pre-check=0")
cchm := ParseCacheControl(hdrs)
fmt.Println(cchm.Has("no-store"))
fmt.Println(cchm.Has("no-cache"))
fmt.Println(cchm.Has("must-revalidate"))
fmt.Println(cchm["post-check"])
fmt.Println(cchm["pre-check"])
// Output: true
// true
// true
// 0
// 0
}
func ExampleCacheControlMap_Has() {
hdrs := http.Header{}
hdrs.Set(CacheControl, "no-cache")
ccm := ParseCacheControl(hdrs)
if ccm.Has("no-cache") {
fmt.Println("Has 'no-cache'")
}
if ccm.Has("no-store") {
fmt.Println("Has 'no-store'")
}
// Output: Has 'no-cache'
}
func TestParseCacheControl(t *testing.T) {
hdrs := http.Header{}
ccStr := "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"
hdrs.Set(CacheControl, ccStr)
cc := ParseCacheControl(hdrs)
if len(cc) != 5 {
t.Errorf("Incorrect number of parameters parsed from '%s'; expected: 5, actual: %d", ccStr, len(cc))
}
if _, ok := cc["no-store"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'no-store' parameter, but it didn't", ccStr)
}
if _, ok := cc["no-cache"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'no-cache' parameter, but it didn't", ccStr)
}
if _, ok := cc["must-revalidate"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'must-revalidate' parameter, but it didn't", ccStr)
}
if pc, ok := cc["post-check"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'post-check' parameter, but it didn't", ccStr)
} else if pc != "0" {
t.Errorf("Invalid value for 'post-check' parsed from '%s'; expected: '0', actual: '%s'", ccStr, pc)
}
if pc, ok := cc["pre-check"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'pre-check' parameter, but it didn't", ccStr)
} else if pc != "0" {
t.Errorf("Invalid value for 'pre-check' parsed from '%s'; expected: '0', actual: '%s'", ccStr, pc)
}
ccStr = "no-store, no-cache"
hdrs.Set(CacheControl, ccStr)
cc = ParseCacheControl(hdrs)
if len(cc) != 2 {
t.Errorf("Incorrect number of parameters parsed from '%s'; expected: 2, actual: %d", ccStr, len(cc))
}
if _, ok := cc["no-store"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'no-store' parameter, but it didn't", ccStr)
}
if _, ok := cc["no-cache"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'no-cache' parameter, but it didn't", ccStr)
}
ccStr = "no-cache"
hdrs.Set(CacheControl, ccStr)
cc = ParseCacheControl(hdrs)
if len(cc) != 1 {
t.Errorf("Incorrect number of parameters parsed from '%s'; expected: 1, actual: %d", ccStr, len(cc))
}
if _, ok := cc["no-cache"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'no-cache' parameter, but it didn't", ccStr)
}
ccStr = ""
hdrs.Set(CacheControl, ccStr)
cc = ParseCacheControl(hdrs)
if len(cc) != 0 {
t.Errorf("Incorrect number of parameters parsed from '%s'; expected: 0, actual: %d", ccStr, len(cc))
}
ccStr = `foo="bar"`
hdrs.Set(CacheControl, ccStr)
cc = ParseCacheControl(hdrs)
if len(cc) != 1 {
t.Errorf("Incorrect number of parameters parsed from '%s'; expected: 1, actual: %d", ccStr, len(cc))
}
if foo, ok := cc["foo"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'foo' parameter, but it didn't", ccStr)
} else if foo != "bar" {
t.Errorf("Invalid value for 'foo' parsed from '%s'; expected: 'bar', actual: '%s'", ccStr, foo)
}
ccStr = `foo="ba\"r"`
hdrs.Set(CacheControl, ccStr)
cc = ParseCacheControl(hdrs)
if len(cc) != 1 {
t.Errorf("Incorrect number of parameters parsed from '%s'; expected: 1, actual: %d", ccStr, len(cc))
}
if foo, ok := cc["foo"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'foo' parameter, but it didn't", ccStr)
} else if foo != `ba"r` {
t.Errorf(`Invalid value for 'foo' parsed from '%s'; expected: 'ba"r', actual: '%s'`, ccStr, foo)
}
ccStr = `foo="ba\"r", baz=blee, aaaa="bb\"\"\"", cc="dd", ee="ff\"f", gg=hh", i="", j="k", l="m\\\\o\"`
hdrs.Set(CacheControl, ccStr)
cc = ParseCacheControl(hdrs)
if len(cc) != 9 {
t.Errorf("Incorrect number of parameters parsed from '%s'; expected: 9, actual: %d", ccStr, len(cc))
}
if foo, ok := cc["foo"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'foo' parameter, but it didn't", ccStr)
} else if foo != `ba"r` {
t.Errorf(`Invalid value for 'foo' parsed from '%s'; expected: 'ba"r', actual: '%s'`, ccStr, foo)
}
if baz, ok := cc["baz"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'baz' parameter, but it didn't", ccStr)
} else if baz != "blee" {
t.Errorf("Invalid value for 'baz' parsed from '%s'; expected: 'blee', actual: '%s'", ccStr, baz)
}
if aaaa, ok := cc["aaaa"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'aaaa' parameter, but it didn't", ccStr)
} else if aaaa != `bb"""` {
t.Errorf(`Invalid value for 'aaaa' parsed from '%s'; expected: 'b"""', actual: '%s'`, ccStr, aaaa)
}
if ccParam, ok := cc["cc"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'cc' parameter, but it didn't", ccStr)
} else if ccParam != "dd" {
t.Errorf("Invalid value for 'cc' parsed from '%s'; expected: 'dd', actual: '%s'", ccStr, ccParam)
}
if ee, ok := cc["ee"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'ee' parameter, but it didn't", ccStr)
} else if ee != `ff"f` {
t.Errorf(`Invalid value for 'ee' parsed from '%s'; expected: 'ff"f', actual: '%s'`, ccStr, ee)
}
if gg, ok := cc["gg"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'gg' parameter, but it didn't", ccStr)
} else if gg != `hh"` {
t.Errorf(`Invalid value for 'gg' parsed from '%s'; expected: 'hh"', actual: '%s'`, ccStr, gg)
}
if i, ok := cc["i"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'i' parameter, but it didn't", ccStr)
} else if i != "" {
t.Errorf("Invalid value for 'i' parsed from '%s'; expected: '', actual: '%s'", ccStr, i)
}
if j, ok := cc["j"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'j' parameter, but it didn't", ccStr)
} else if j != "k" {
t.Errorf("Invalid value for 'j' parsed from '%s'; expected: 'k', actual: '%s'", ccStr, j)
}
if l, ok := cc["l"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'l' parameter, but it didn't", ccStr)
} else if l != `m\\o\` {
t.Errorf(`Invalid value for 'l' parsed from '%s'; expected: 'm\\o\', actual: '%s'`, ccStr, l)
}
ccStr = `foo="ba\"r", baz`
hdrs.Set(CacheControl, ccStr)
cc = ParseCacheControl(hdrs)
if len(cc) != 2 {
t.Errorf("Incorrect number of parameters parsed from '%s'; expected: 2, actual: %d", ccStr, len(cc))
}
if foo, ok := cc["foo"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'foo' parameter, but it didn't", ccStr)
} else if foo != `ba"r` {
t.Errorf(`Invalid value for 'foo' parsed from '%s'; expected: 'ba"r', actual: '%s'`, ccStr, foo)
}
if _, ok := cc["baz"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'baz' parameter, but it didn't", ccStr)
}
ccStr = "foo="
hdrs.Set(CacheControl, ccStr)
cc = ParseCacheControl(hdrs)
if len(cc) != 1 {
t.Errorf("Incorrect number of parameters parsed from '%s'; expected: 1, actual: %d", ccStr, len(cc))
}
if foo, ok := cc["foo"]; !ok {
t.Errorf("Expected parsed map from '%s' to have 'foo' parameter, but it didn't", ccStr)
} else if foo != "" {
t.Errorf("Invalid value for 'foo' parsed from '%s'; expected: '', actual: '%s'", ccStr, foo)
}
}
func BenchmarkParseCacheControl(b *testing.B) {
hdrs := http.Header{}
ccStr := `foo="ba\"r", baz=blee, aaaa="bb\"\"\"", cc="dd", ee="ff\"f", gg=hh", i="", j="k", l="m\\\\o\"`
hdrs.Set(CacheControl, ccStr)
for i := 0; i < b.N; i++ {
ParseCacheControl(hdrs)
}
}
func TestCanCache(t *testing.T) {
// tests RFC7234§5.2.1.5 compliance
t.Run("client no-store with strict RFC", func(t *testing.T) {
reqHdr := http.Header{
"Cache-Control": {"no-store"},
}
respCode := http.StatusOK
respHdr := http.Header{}
strictRFC := true
if CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned true for no-store request and strict RFC")
}
})
// tests RFC7234§5.2.1.5 violation to protect origins
t.Run("client no-store without strict RFC", func(t *testing.T) {
reqHdr := http.Header{
"Cache-Control": {"no-store"},
}
respCode := 200
respHdr := http.Header{}
strictRFC := false
if !CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned false for no-store request and strict RFC disabled")
}
})
// tests RFC7234§5.2.1.5 violation to protect origins
t.Run("client no-cache no-store without strict RFC", func(t *testing.T) {
reqHdr := http.Header{
"Cache-Control": {"no-cache no-store"},
}
respCode := 200
respHdr := http.Header{}
strictRFC := false
if !CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned false for no-store request and strict RFC disabled")
}
})
t.Run("parent no-cache with strict RFC", func(t *testing.T) {
reqHdr := http.Header{}
respCode := 200
respHdr := http.Header{
"Cache-Control": {"no-cache"},
}
strictRFC := false
if CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned true for no-cache request and strict RFC disabled")
}
})
t.Run("parent no-store with strict RFC", func(t *testing.T) {
reqHdr := http.Header{}
respCode := 200
respHdr := http.Header{
"Cache-Control": {"no-store"},
}
strictRFC := false
if CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned true for no-cache request and strict RFC disabled")
}
})
t.Run("parent no-cache without strict RFC", func(t *testing.T) {
reqHdr := http.Header{}
respCode := 200
respHdr := http.Header{
"Cache-Control": {"no-cache"},
}
strictRFC := false
if CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned true for no-cache request and strict RFC enabled")
}
})
t.Run("parent no-store without strict RFC", func(t *testing.T) {
reqHdr := http.Header{}
respCode := 200
respHdr := http.Header{
"Cache-Control": {"no-store"},
}
strictRFC := true
if CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned true for no-cache request and strict RFC enabled")
}
})
// tests RFC7234§5.2.1.3 and RFC7231§6.1 compliance
t.Run("cache-able response codes", func(t *testing.T) {
defaultCacheableCodes := []int{200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501} // RFC7231§6.1
for _, code := range defaultCacheableCodes {
reqHdr := http.Header{}
respCode := code
respHdr := http.Header{}
strictRFC := true
if !CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned false for request with no cache control and default-cacheable response code %v", code)
}
}
})
// tests RFC7234§5.2.1.3 compliance
t.Run("non-cache-able response codes without Cache-Control", func(t *testing.T) {
nonDefaultCacheableCodes := []int{
201, 202, 205, 207, 208, 226,
302, 303, 304, 305, 306, 307, 308,
400, 401, 402, 403, 406, 407, 408, 409, 411, 412, 413, 4015, 416, 417, 418, 421, 422, 423, 424, 428, 429, 431, 451,
500, 502, 503, 504, 505, 506, 507, 508, 510, 511,
}
for _, code := range nonDefaultCacheableCodes {
reqHdr := http.Header{}
respCode := code
respHdr := http.Header{}
strictRFC := true
if CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned true for request with no cache control and non-default-cacheable response code %v", code)
}
}
})
// tests RFC7234§3 compliance
t.Run("non-cache-able response codes with Cache-Control", func(t *testing.T) {
nonDefaultCacheableCodes := []int{
201, 202, 205, 207, 208, 226,
302, 303, 304, 305, 306, 307, 308,
400, 401, 402, 403, 406, 407, 408, 409, 411, 412, 413, 4015, 416, 417, 418, 421, 422, 423, 424, 428, 429, 431, 451,
500, 502, 503, 504, 505, 506, 507, 508, 510, 511,
}
cacheableRespHdrs := []map[string][]string{
{"Expires": {time.Now().Format(time.RFC1123)}},
{"Cache-Control": {"max-age=42"}},
{"Cache-Control": {"s-maxage=42"}},
}
for _, code := range nonDefaultCacheableCodes {
for _, hdr := range cacheableRespHdrs {
reqHdr := http.Header{}
respCode := code
respHdr := hdr
strictRFC := true
if !CanCache(http.MethodGet, reqHdr, respCode, respHdr, strictRFC) {
t.Errorf("CanCache returned false for request with non-default-cacheable response code %v and cacheable header %v", respCode, respHdr)
}
}
}
})
}
// This benchmarks one of the longer checking processes: when the response code
// is not cache-able by default, but a Cache-Control header is present that
// makes it cache-able.
func BenchmarkCanCache(b *testing.B) {
hdrs := http.Header{}
hdrs.Set(CacheControl, "s-maxage=42")
for i := 0; i < b.N; i++ {
CanCache(http.MethodGet, http.Header{}, http.StatusCreated, hdrs, true)
}
}
func TestCanReuseStored(t *testing.T) {
// tests RFC7234§5.2.1.4 violation to protect origins
t.Run("test client no-cache is ignored without strict RFC", func(t *testing.T) {
reqHdr := http.Header{
"Cache-Control": {"no-cache"},
}
respHdr := http.Header{}
reqCC := CacheControlMap{
"no-cache": "",
}
respCC := CacheControlMap{}
respReqHdrs := http.Header{}
respReqTime := time.Now()
respRespTime := time.Now()
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored for no-cache request and strict RFC disabled: expected ReuseCan, actual %v", reuse)
}
})
// tests RFC7234§5.2.1.4 violation to protect origins
t.Run("test client no-store is ignored without strict RFC", func(t *testing.T) {
reqHdr := http.Header{
"Cache-Control": {"no-store no-cache"},
}
respHdr := http.Header{}
reqCC := CacheControlMap{
"no-store": "",
"no-cache": "",
}
respCC := CacheControlMap{}
respReqHdrs := http.Header{}
respReqTime := time.Now()
respRespTime := time.Now()
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored for no-cache request and strict RFC disabled: expected ReuseCan, actual %v", reuse)
}
})
// tests RFC7234§5.2.1.4 violation to protect origins
t.Run("test client no-cache and no-store is ignored without strict RFC", func(t *testing.T) {
reqHdr := http.Header{
"Cache-Control": {"no-store no-cache"},
}
respHdr := http.Header{}
reqCC := CacheControlMap{
"no-store": "",
"no-cache": "",
}
respCC := CacheControlMap{}
respReqHdrs := http.Header{}
respReqTime := time.Now()
respRespTime := time.Now()
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored for no-cache request and strict RFC disabled: expected ReuseCan, actual %v", reuse)
}
})
t.Run("test parent Expires in future is reused", func(t *testing.T) {
now := time.Now()
tenMinsBeforeExpires := now.Add(time.Minute * -10)
expires := now.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Expires": {expires},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{}
respReqHdrs := http.Header{}
respReqTime := tenMinsBeforeExpires
respRespTime := tenMinsBeforeExpires
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored request after expires: expected ReuseCan, actual %v", reuse)
}
})
t.Run("test parent Expires in past has revaldiate and can stale", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
expires := tenMinutesAgo.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Expires": {expires},
"Date": {expires},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{}
respReqHdrs := http.Header{}
respReqTime := now
respRespTime := now
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidateCanStale {
t.Errorf("CanReuseStored request after expires: expected ReuseMustRevalidateCanStale, actual %v", reuse)
}
})
t.Run("test parent Expires in past with must-revaldiate, cannot stale", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
expires := tenMinutesAgo.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Expires": {expires},
"Date": {expires},
"Cache-Control": {"must-revalidate"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"must-revalidate": ""}
respReqHdrs := http.Header{}
respReqTime := now
respRespTime := now
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidate {
t.Errorf("CanReuseStored request after expires and response must-revalidate: expected ReuseMustRevalidate, actual %v", reuse)
}
})
t.Run("test parent Expires in past proxy-revaldiate, and no-stale", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
expires := tenMinutesAgo.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Expires": {expires},
"Date": {expires},
"Cache-Control": {"must-revalidate"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"must-revalidate": ""}
respReqHdrs := http.Header{}
respReqTime := now
respRespTime := now
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidate {
t.Errorf("CanReuseStored request after expires and response must-revalidate: expected ReuseMustRevalidate, actual %v", reuse)
}
})
t.Run("test parent Expires in past with proxy-revaldiate returns MustRevalidateNoStale", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
expires := tenMinutesAgo.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Expires": {expires},
"Date": {expires},
"Cache-Control": {"proxy-revalidate"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"proxy-revalidate": ""}
respReqHdrs := http.Header{}
respReqTime := now
respRespTime := now
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidate {
t.Errorf("CanReuseStored request after expires and response must-revalidate: expected ReuseMustRevalidate, actual %v", reuse)
}
})
t.Run("test parent max-age in future returns CanReuse", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"max-age=1200"}, // 20 minutes
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"max-age": "1200"}
respReqHdrs := http.Header{}
respReqTime := now
respRespTime := now
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored request after expires and response must-revalidate: expected ReuseCan, actual %v", reuse)
}
})
t.Run("test parent max-age in past returns MustRevalidate", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"max-age=300"}, // 5 minutes
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"max-age": "300"}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidateCanStale {
t.Errorf("CanReuseStored request after response max-age: expected ReuseMustRevalidateCanStale, actual %v", reuse)
}
})
t.Run("test parent s-maxage in future returns CanReuse", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"s-maxage=1200"}, // 20 minutes
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"s-maxage": "1200"}
respReqHdrs := http.Header{}
respReqTime := now
respRespTime := now
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored request before s-maxage: expected ReuseCan, actual %v", reuse)
}
})
t.Run("test parent s-age in past returns MustRevalidate", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"s-maxage=300"}, // 5 minutes
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"s-maxage": "300"}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidateCanStale {
t.Errorf("CanReuseStored request after response s-maxage: expected ReuseMustRevalidateCanStale, actual %v", reuse)
}
})
t.Run("test parent future s-maxage overrides past max-age and returns CanReuse", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"max-age=300,s-maxage=1200"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"s-maxage": "1200",
"max-age": "300",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored request before s-maxage but after max-age: expected ReuseCan, actual %v", reuse)
}
})
t.Run("test parent past s-maxage overrides future max-age and returns MustReval", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"max-age=1200,s-maxage=300"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"s-maxage": "300",
"max-age": "1200",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidateCanStale {
t.Errorf("CanReuseStored request before s-maxage but after max-age: expected ReuseMustRevalidateCanStale, actual %v", reuse)
}
})
t.Run("test parent future max-age overrides past Expires and returns CanReuse", func(t *testing.T) {
now := time.Now()
twentyMinutesAgo := now.Add(time.Minute * -10)
tenMinutesAgo := now.Add(time.Minute * -10)
expires := twentyMinutesAgo.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Expires": {expires},
"Cache-Control": {"max-age=1200"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"max-age": "1200",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored request before max-age but after Expires: expected ReuseCan, actual %v", reuse)
}
})
t.Run("test parent past max-age overrides future Expires and returns MustRevalidate", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
fiveMinutesHence := now.Add(time.Minute * 5)
expires := fiveMinutesHence.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Expires": {expires},
"Cache-Control": {"max-age=300"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"max-age": "300",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidateCanStale {
t.Errorf("CanReuseStored request after max-age but before Expires: expected ReuseMustRevalidateCanStale, actual %v", reuse)
}
})
t.Run("test parent future s-maxage overrides past Expires and returns CanReuse", func(t *testing.T) {
now := time.Now()
twentyMinutesAgo := now.Add(time.Minute * -10)
tenMinutesAgo := now.Add(time.Minute * -10)
expires := twentyMinutesAgo.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Expires": {expires},
"Cache-Control": {"s-maxage=1200"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"s-maxage": "1200",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored request before s-maxage but after Expires: expected ReuseCan, actual %v", reuse)
}
})
t.Run("test parent past s-maxage overrides future Expires and returns MustRevalidate", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
fiveMinutesHence := now.Add(time.Minute * 5)
expires := fiveMinutesHence.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Expires": {expires},
"Cache-Control": {"s-maxage=300"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"s-maxage": "300",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidateCanStale {
t.Errorf("CanReuseStored request after max-age but before Expires: expected ReuseMustRevalidateCanStale, actual %v", reuse)
}
})
t.Run("test parent future s-maxage overrides past Expires and past max-age and returns CanReuse", func(t *testing.T) {
now := time.Now()
twentyMinutesAgo := now.Add(time.Minute * -10)
tenMinutesAgo := now.Add(time.Minute * -10)
expires := twentyMinutesAgo.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Expires": {expires},
"Cache-Control": {"s-maxage=1200,max-age=300"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"s-maxage": "1200",
"max-age": "300",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored request before s-maxage but after Expires: expected ReuseCan, actual %v", reuse)
}
})
t.Run("test parent past s-maxage overrides future Expires and future max-age and returns MustRevalidate", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
fiveMinutesHence := now.Add(time.Minute * 5)
expires := fiveMinutesHence.Format(time.RFC1123)
reqHdr := http.Header{}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Expires": {expires},
"Cache-Control": {"s-maxage=300,max-age=1200"},
}
reqCC := CacheControlMap{}
respCC := CacheControlMap{
"s-maxage": "300",
"max-age": "1200",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidateCanStale {
t.Errorf("CanReuseStored request after s-maxage but before Expires: expected ReuseMustRevalidateCanStale, actual %v", reuse)
}
})
// tests RFC7234§5.2.1.3 compliance
t.Run("test client min-fresh is obeyed with strict RFC", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
reqHdr := http.Header{
"Cache-Control": {"min-fresh=900"},
}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"max-age=1200"},
}
reqCC := CacheControlMap{
"min-fresh": "900",
}
respCC := CacheControlMap{
"max-age": "1200",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := true
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseMustRevalidate {
t.Errorf("CanReuseStored request with strictRFC min-fresh 300 with 600 remaining: expected ReuseMustRevalidate, actual %v", reuse)
}
})
// tests RFC7234§5.2.1.3 violation to protect origins
t.Run("test client min-fresh is ignored without strict RFC", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
reqHdr := http.Header{
"Cache-Control": {"min-fresh=900"},
}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"max-age=1200"},
}
reqCC := CacheControlMap{
"min-fresh": "900",
}
respCC := CacheControlMap{
"max-age": "1200",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored request with strictRFC min-fresh 1200 with 600 remaining: expected ReuseCan, actual %v", reuse)
}
})
// tests RFC7234§5.2.1.3 violation to protect origins
t.Run("test client min-fresh is ignored without strict RFC", func(t *testing.T) {
now := time.Now()
tenMinutesAgo := now.Add(time.Minute * -10)
reqHdr := http.Header{
"Cache-Control": {"min-fresh=900"},
}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"max-age=1200"},
}
reqCC := CacheControlMap{
"min-fresh": "900",
}
respCC := CacheControlMap{
"max-age": "1200",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := false
if reuse := CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC); reuse != ReuseCan {
t.Errorf("CanReuseStored request with strictRFC min-fresh 1200 with 600 remaining: expected ReuseCan, actual %v", reuse)
}
})
}
func BenchmarkCanReuseStored(b *testing.B) {
tenMinutesAgo := time.Now().Add(time.Minute * -10)
reqHdr := http.Header{
"Cache-Control": {"min-fresh=900"},
}
respHdr := http.Header{
"Date": {tenMinutesAgo.Format(time.RFC1123)},
"Cache-Control": {"max-age=1200"},
}
reqCC := CacheControlMap{
"min-fresh": "900",
}
respCC := CacheControlMap{
"max-age": "1200",
}
respReqHdrs := http.Header{}
respReqTime := tenMinutesAgo
respRespTime := tenMinutesAgo
strictRFC := true
for i := 0; i < b.N; i++ {
CanReuseStored(reqHdr, respHdr, reqCC, respCC, respReqHdrs, respReqTime, respRespTime, strictRFC)
}
}