diff --git a/Caddyfile b/Caddyfile
index 5f2d521..fde988b 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -287,6 +287,15 @@ route /custom-key/without-* {
respond "Hello to the authenticated user."
}
+route /must-revalidate {
+ cache {
+ ttl 5s
+ stale 5s
+ }
+ header Cache-Control "must-revalidate"
+ reverse_proxy 127.0.0.1:81
+}
+
route /cache-authorization {
cache {
cache_keys {
@@ -299,6 +308,49 @@ route /cache-authorization {
respond "Hello to the authenticated user."
}
+route /bypass {
+ cache {
+ mode bypass
+ }
+
+ header Cache-Control "no-store"
+ respond "Hello bypass"
+}
+
+route /bypass_request {
+ cache {
+ mode bypass_request
+ }
+
+ respond "Hello bypass_request"
+}
+
+route /bypass_response {
+ cache {
+ mode bypass_response
+ }
+
+ header Cache-Control "no-cache, no-store"
+ respond "Hello bypass_response"
+}
+
+route /strict_request {
+ cache {
+ mode strict
+ }
+
+ respond "Hello strict"
+}
+
+route /strict_response {
+ cache {
+ mode strict
+ }
+
+ header Cache-Control "no-cache, no-store"
+ respond "Hello strict"
+}
+
cache @souin-api {
}
diff --git a/README.md b/README.md
index 8f3dbef..d36631c 100644
--- a/README.md
+++ b/README.md
@@ -79,6 +79,7 @@ Here are all the available options for the global options
headers Content-Type Authorization
}
log_level debug
+ mode bypass
nuts {
path /path/to/the/storage
}
@@ -384,6 +385,7 @@ What does these directives mean?
| `key.disable_query` | Disable the query string part in the key | `true`
`(default: false)` |
| `key.headers` | Add headers to the key matching the regexp | `Authorization Content-Type X-Additional-Header` |
| `key.hide` | Prevent the key from being exposed in the `Cache-Status` HTTP response header | `true`
`(default: false)` |
+| `mode` | Bypass the RFC respect | One of `bypass` `bypass_request` `bypass_response` `strict` (default `strict`) |
| `nuts` | Configure the Nuts cache storage | |
| `nuts.path` | Set the Nuts file path storage | `/anywhere/nuts/storage` |
| `nuts.configuration` | Configure Nuts directly in the Caddyfile or your JSON caddy configuration | [See the Nuts configuration for the options](https://github.com/nutsdb/nutsdb#default-options) |
diff --git a/configuration.go b/configuration.go
index df4f612..93c0f06 100644
--- a/configuration.go
+++ b/configuration.go
@@ -28,6 +28,8 @@ type DefaultCache struct {
Headers []string `json:"headers"`
// Configure the global key generation.
Key configurationtypes.Key `json:"key"`
+ // Mode defines if strict or bypass.
+ Mode string `json:"mode"`
// Olric provider configuration.
Olric configurationtypes.CacheProvider `json:"olric"`
// Redis provider configuration.
@@ -86,6 +88,11 @@ func (d *DefaultCache) GetEtcd() configurationtypes.CacheProvider {
return d.Etcd
}
+// GetMode returns mdoe configuration
+func (d *DefaultCache) GetMode() string {
+ return d.Mode
+}
+
// GetNuts returns nuts configuration
func (d *DefaultCache) GetNuts() configurationtypes.CacheProvider {
return d.Nuts
@@ -274,6 +281,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
switch directive {
case "basepath":
apiConfiguration.Debug.BasePath = h.RemainingArgs()[0]
+ default:
+ return h.Errf("unsupported debug directive: %s", directive)
}
}
case "prometheus":
@@ -284,6 +293,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
switch directive {
case "basepath":
apiConfiguration.Prometheus.BasePath = h.RemainingArgs()[0]
+ default:
+ return h.Errf("unsupported prometheus directive: %s", directive)
}
}
case "souin":
@@ -294,8 +305,12 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
switch directive {
case "basepath":
apiConfiguration.Souin.BasePath = h.RemainingArgs()[0]
+ default:
+ return h.Errf("unsupported souin directive: %s", directive)
}
}
+ default:
+ return h.Errf("unsupported api directive: %s", directive)
}
}
cfg.API = apiConfiguration
@@ -310,6 +325,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
case "configuration":
provider.Configuration = parseCaddyfileRecursively(h)
provider.Configuration = parseBadgerConfiguration(provider.Configuration.(map[string]interface{}))
+ default:
+ return h.Errf("unsupported badger directive: %s", directive)
}
}
cfg.DefaultCache.Badger = provider
@@ -337,6 +354,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
ck.Hide = true
case "headers":
ck.Headers = h.RemainingArgs()
+ default:
+ return h.Errf("unsupported cache_keys (%s) directive: %s", rg, directive)
}
}
@@ -367,6 +386,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
cdn.Provider = h.RemainingArgs()[0]
case "strategy":
cdn.Strategy = h.RemainingArgs()[0]
+ default:
+ return h.Errf("unsupported cdn directive: %s", directive)
}
}
cfg.DefaultCache.CDN = cdn
@@ -381,6 +402,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
switch directive {
case "configuration":
provider.Configuration = parseCaddyfileRecursively(h)
+ default:
+ return h.Errf("unsupported etcd directive: %s", directive)
}
}
cfg.DefaultCache.Etcd = provider
@@ -403,12 +426,20 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
config_key.Hide = true
case "headers":
config_key.Headers = h.RemainingArgs()
+ default:
+ return h.Errf("unsupported key directive: %s", directive)
}
}
cfg.DefaultCache.Key = config_key
case "log_level":
args := h.RemainingArgs()
cfg.LogLevel = args[0]
+ case "mode":
+ args := h.RemainingArgs()
+ if len(args) > 1 {
+ return h.Errf("mode must contains only one arg: %s given", args)
+ }
+ cfg.DefaultCache.Mode = args[0]
case "nuts":
provider := configurationtypes.CacheProvider{}
for nesting := h.Nesting(); h.NextBlock(nesting); {
@@ -422,6 +453,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
provider.Path = urlArgs[0]
case "configuration":
provider.Configuration = parseCaddyfileRecursively(h)
+ default:
+ return h.Errf("unsupported nuts directive: %s", directive)
}
}
cfg.DefaultCache.Nuts = provider
@@ -439,6 +472,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
provider.Path = urlArgs[0]
case "configuration":
provider.Configuration = parseCaddyfileRecursively(h)
+ default:
+ return h.Errf("unsupported olric directive: %s", directive)
}
}
cfg.DefaultCache.Olric = provider
@@ -457,6 +492,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
case "configuration":
provider.Configuration = parseCaddyfileRecursively(h)
provider.Configuration = parseRedisConfiguration(provider.Configuration.(map[string]interface{}))
+ default:
+ return h.Errf("unsupported redis directive: %s", directive)
}
}
cfg.DefaultCache.Redis = provider
@@ -466,6 +503,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
switch directive {
case "exclude":
cfg.DefaultCache.Regex.Exclude = h.RemainingArgs()[0]
+ default:
+ return h.Errf("unsupported regex directive: %s", directive)
}
}
case "stale":
@@ -493,6 +532,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
d.Duration = ttl
}
timeout.Cache = d
+ default:
+ return h.Errf("unsupported timeout directive: %s", directive)
}
}
cfg.DefaultCache.Timeout = timeout
@@ -503,9 +544,7 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isBlocking b
cfg.DefaultCache.TTL.Duration = ttl
}
default:
- if isBlocking {
- return h.Errf("unsupported root directive: %s", rootOption)
- }
+ return h.Errf("unsupported root directive: %s", rootOption)
}
}
}
diff --git a/go.mod b/go.mod
index e4b8d28..31a10b6 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.19
require (
github.com/buraksezer/olric v0.5.4
github.com/caddyserver/caddy/v2 v2.6.4
- github.com/darkweak/souin v1.6.36
+ github.com/darkweak/souin v1.6.39
go.uber.org/zap v1.24.0
)
@@ -102,7 +102,7 @@ require (
github.com/mschoch/smat v0.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/pquerna/cachecontrol v0.1.0 // indirect
+ github.com/pquerna/cachecontrol v0.1.1-0.20230415224848-baaf0ee61529 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.41.0 // indirect
diff --git a/go.sum b/go.sum
index bf16fec..75d9334 100644
--- a/go.sum
+++ b/go.sum
@@ -656,8 +656,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/darkweak/go-esi v0.0.5 h1:b9LHI8Tz46R+i6p8avKPHAIBRQUCZDebNmKm5w/Zrns=
github.com/darkweak/go-esi v0.0.5/go.mod h1:koCJqwum1u6mslyZuq/Phm6hfG1K3ZK5Y7jrUBTH654=
-github.com/darkweak/souin v1.6.36 h1:T+7xZFjnDgbjOPF/1jEFj3cBcTMK6hzdG0X3iQiGkGg=
-github.com/darkweak/souin v1.6.36/go.mod h1:/C9ISWex+bkdmHgAT2wZpq9VQddcMLb0i8/ykrapN40=
+github.com/darkweak/souin v1.6.39 h1:uVO18+pvy5N8dJH+CCfuBD3gx7AXWaBv+RjC9ee1pCk=
+github.com/darkweak/souin v1.6.39/go.mod h1:7B7VqPbFrF7AWJDRP6FCbE+zGiRUUqHAmWOs3Q2n4hk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -1115,8 +1115,8 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
-github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
+github.com/pquerna/cachecontrol v0.1.1-0.20230415224848-baaf0ee61529 h1:wcNVCAIsWcLpEJ5FhXHKC7dBi6SIZQukrOTY5eHes0M=
+github.com/pquerna/cachecontrol v0.1.1-0.20230415224848-baaf0ee61529/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
diff --git a/httpcache_test.go b/httpcache_test.go
index 41d6ecf..1eb9a4a 100644
--- a/httpcache_test.go
+++ b/httpcache_test.go
@@ -248,7 +248,7 @@ func TestNotHandledRoute(t *testing.T) {
}`, "caddyfile")
resp1, _ := tester.AssertGetResponse(`http://localhost:9080/not-handled`, 200, "Hello, Age header!")
- if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; detail=EXCLUDED-REQUEST-URI" {
+ if resp1.Header.Get("Cache-Status") != "Souin; fwd=bypass; detail=EXCLUDED-REQUEST-URI" {
t.Errorf("unexpected Cache-Status header value %v", resp1.Header.Get("Cache-Status"))
}
}
@@ -329,3 +329,104 @@ func TestAuthenticatedRoute(t *testing.T) {
t.Errorf("unexpected Cache-Status header %v", respAuthVaryBypassAlice2.Header.Get("Cache-Status"))
}
}
+
+type testErrorHandler struct {
+ iterator int
+}
+
+func (t *testErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ t.iterator++
+ if t.iterator%2 == 0 {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Cache-Control", "must-revalidate")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte("Hello must-revalidate!"))
+}
+
+func TestMustRevalidate(t *testing.T) {
+ tester := caddytest.NewTester(t)
+ tester.InitServer(`
+ {
+ admin localhost:2999
+ order cache before rewrite
+ http_port 9080
+ cache {
+ ttl 5s
+ stale 5s
+ }
+ }
+ localhost:9080 {
+ route /cache-default {
+ cache
+ reverse_proxy localhost:9081
+ }
+ }`, "caddyfile")
+
+ errorHandler := testErrorHandler{}
+ go func(teh *testErrorHandler) {
+ _ = http.ListenAndServe(":9081", teh)
+ }(&errorHandler)
+ time.Sleep(time.Second)
+ resp1, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, http.StatusOK, "Hello must-revalidate!")
+ resp2, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, http.StatusOK, "Hello must-revalidate!")
+ time.Sleep(6 * time.Second)
+ staleReq, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/cache-default", nil)
+ staleReq.Header = http.Header{"Cache-Control": []string{"max-stale=3, stale-if-error=84600"}}
+ resp3, _ := tester.AssertResponse(staleReq, http.StatusOK, "Hello must-revalidate!")
+
+ if resp1.Header.Get("Cache-Control") != "must-revalidate" {
+ t.Errorf("unexpected resp1 Cache-Control header %v", resp1.Header.Get("Cache-Control"))
+ }
+ if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-default" {
+ t.Errorf("unexpected resp1 Cache-Status header %v", resp1.Header.Get("Cache-Status"))
+ }
+ if resp1.Header.Get("Age") != "" {
+ t.Errorf("unexpected resp1 Age header %v", resp1.Header.Get("Age"))
+ }
+
+ if resp2.Header.Get("Cache-Control") != "must-revalidate" {
+ t.Errorf("unexpected resp2 Cache-Control header %v", resp2.Header.Get("Cache-Control"))
+ }
+ if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/cache-default" {
+ t.Errorf("unexpected resp2 Cache-Status header %v", resp2.Header.Get("Cache-Status"))
+ }
+ if resp2.Header.Get("Age") != "1" {
+ t.Errorf("unexpected resp2 Age header %v", resp2.Header.Get("Age"))
+ }
+
+ if resp3.Header.Get("Cache-Control") != "must-revalidate" {
+ t.Errorf("unexpected resp3 Cache-Control header %v", resp3.Header.Get("Cache-Control"))
+ }
+ if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=-2; key=GET-http-localhost:9080-/cache-default; fwd=stale; fwd-status=500" {
+ t.Errorf("unexpected resp3 Cache-Status header %v", resp3.Header.Get("Cache-Status"))
+ }
+ if resp3.Header.Get("Age") != "7" {
+ t.Errorf("unexpected resp3 Age header %v", resp3.Header.Get("Age"))
+ }
+
+ resp4, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, http.StatusOK, "Hello must-revalidate!")
+ if resp4.Header.Get("Cache-Control") != "must-revalidate" {
+ t.Errorf("unexpected resp4 Cache-Control header %v", resp4.Header.Get("Cache-Control"))
+ }
+ if resp4.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-default" {
+ t.Errorf("unexpected resp4 Cache-Status header %v", resp4.Header.Get("Cache-Status"))
+ }
+ if resp4.Header.Get("Age") != "" {
+ t.Errorf("unexpected resp4 Age header %v", resp4.Header.Get("Age"))
+ }
+
+ time.Sleep(6 * time.Second)
+ staleReq, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/cache-default", nil)
+ staleReq.Header = http.Header{"Cache-Control": []string{"max-stale=3"}}
+ resp5, _ := tester.AssertResponse(staleReq, http.StatusGatewayTimeout, "")
+
+ if resp5.Header.Get("Cache-Status") != "Souin; fwd=request; fwd-status=500; key=GET-http-localhost:9080-/cache-default; detail=REQUEST-REVALIDATION" {
+ t.Errorf("unexpected resp5 Cache-Status header %v", resp4.Header.Get("Cache-Status"))
+ }
+ if resp5.Header.Get("Age") != "" {
+ t.Errorf("unexpected resp5 Age header %v", resp4.Header.Get("Age"))
+ }
+}