From 92e0467f77e76e5b7b086bbe1189255cf60c4a90 Mon Sep 17 00:00:00 2001 From: darkweak Date: Thu, 17 Oct 2024 19:02:21 +0200 Subject: [PATCH] fix(chore): stale-if-error --- pkg/middleware/middleware.go | 30 ++++++++- pkg/rfc/age.go | 16 ++++- pkg/rfc/age_test.go | 10 +-- pkg/surrogate/providers/common.go | 7 ++- plugins/caddy/httpcache_test.go | 100 ++++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 8 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 92c909840..53f64b551 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -774,7 +774,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n return err } - if !modeContext.Strict || rfc.ValidateMaxAgeCachedStaleResponse(requestCc, response, int(addTime.Seconds())) != nil { + if !modeContext.Strict || rfc.ValidateMaxAgeCachedStaleResponse(requestCc, responseCc, response, int(addTime.Seconds())) != nil { customWriter.WriteHeader(response.StatusCode) rfc.HitStaleCache(&response.Header) maps.Copy(customWriter.Header(), response.Header) @@ -784,6 +784,34 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n return err } } + } else if stale != nil { + response := stale + addTime, _ := time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader)) + responseCc, _ := cacheobject.ParseResponseCacheControl(rfc.HeaderAllCommaSepValuesString(response.Header, "Cache-Control")) + + if !modeContext.Strict || rfc.ValidateMaxAgeCachedStaleResponse(requestCc, responseCc, response, int(addTime.Seconds())) != nil { + _, _ = time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader)) + rfc.SetCacheStatusHeader(response, storerName) + + responseCc, _ := cacheobject.ParseResponseCacheControl(rfc.HeaderAllCommaSepValuesString(response.Header, "Cache-Control")) + + if responseCc.StaleIfError > -1 || requestCc.StaleIfError > 0 { + err := s.Revalidate(validator, next, customWriter, req, requestCc, cachedKey, uri) + statusCode := customWriter.GetStatusCode() + if err != nil { + code := fmt.Sprintf("; fwd-status=%d", statusCode) + rfc.HitStaleCache(&response.Header) + response.Header.Set("Cache-Status", response.Header.Get("Cache-Status")+code) + maps.Copy(customWriter.Header(), response.Header) + customWriter.WriteHeader(response.StatusCode) + _, _ = io.Copy(customWriter.Buf, response.Body) + _, err := customWriter.Send() + + return err + } + } + + } } } diff --git a/pkg/rfc/age.go b/pkg/rfc/age.go index 03b8593a9..03f12acbc 100644 --- a/pkg/rfc/age.go +++ b/pkg/rfc/age.go @@ -31,11 +31,25 @@ func ValidateMaxAgeCachedResponse(co *cacheobject.RequestCacheDirectives, res *h return validateMaxAgeCachedResponse(res, int(ma), 0) } -func ValidateMaxAgeCachedStaleResponse(co *cacheobject.RequestCacheDirectives, res *http.Response, addTime int) *http.Response { +func ValidateMaxAgeCachedStaleResponse(co *cacheobject.RequestCacheDirectives, resCo *cacheobject.ResponseCacheDirectives, res *http.Response, addTime int) *http.Response { if co.MaxStaleSet { return res } + if resCo != nil && (resCo.StaleIfError > -1 || co.StaleIfError > 0) { + if resCo.StaleIfError > -1 { + if response := validateMaxAgeCachedResponse(res, int(resCo.StaleIfError), addTime); response != nil { + return response + } + } + + if co.StaleIfError > 0 { + if response := validateMaxAgeCachedResponse(res, int(co.StaleIfError), addTime); response != nil { + return response + } + } + } + if co.MaxStale < 0 { return nil } diff --git a/pkg/rfc/age_test.go b/pkg/rfc/age_test.go index c62e3f1e0..b1fc5fe3d 100644 --- a/pkg/rfc/age_test.go +++ b/pkg/rfc/age_test.go @@ -63,20 +63,20 @@ func Test_ValidateMaxStaleCachedResponse(t *testing.T) { }, } - if ValidateMaxAgeCachedStaleResponse(&coWithoutMaxStale, &expiredMaxAge, 3) != nil { + if ValidateMaxAgeCachedStaleResponse(&coWithoutMaxStale, nil, &expiredMaxAge, 3) != nil { t.Errorf("The max-stale validation should return nil instead of the response with the given parameters:\nRequestCacheDirectives: %+v\nResponse: %+v\n", coWithoutMaxStale, expiredMaxAge) } - if ValidateMaxAgeCachedStaleResponse(&coWithoutMaxStale, &validMaxAge, 14) != nil { + if ValidateMaxAgeCachedStaleResponse(&coWithoutMaxStale, nil, &validMaxAge, 14) != nil { t.Errorf("The max-stale validation should return the response instead of nil with the given parameters:\nRequestCacheDirectives: %+v\nResponse: %+v\n", coWithoutMaxStale, validMaxAge) } - if ValidateMaxAgeCachedStaleResponse(&coWithMaxStale, &expiredMaxAge, 0) != nil { + if ValidateMaxAgeCachedStaleResponse(&coWithMaxStale, nil, &expiredMaxAge, 0) != nil { t.Errorf("The max-stale validation should return nil instead of the response with the given parameters:\nRequestCacheDirectives: %+v\nResponse: %+v\n", coWithMaxStale, expiredMaxAge) } - if ValidateMaxAgeCachedStaleResponse(&coWithMaxStaleSet, &expiredMaxAge, 0) == nil { + if ValidateMaxAgeCachedStaleResponse(&coWithMaxStaleSet, nil, &expiredMaxAge, 0) == nil { t.Errorf("The max-stale validation should return the response instead of nil with the given parameters:\nRequestCacheDirectives: %+v\nResponse: %+v\n", coWithMaxStaleSet, expiredMaxAge) } - if ValidateMaxAgeCachedStaleResponse(&coWithMaxStale, &validMaxAge, 5) == nil { + if ValidateMaxAgeCachedStaleResponse(&coWithMaxStale, nil, &validMaxAge, 5) == nil { t.Errorf("The max-stale validation should return the response instead of nil with the given parameters:\nRequestCacheDirectives: %+v\nResponse: %+v\n", coWithMaxStale, expiredMaxAge) } } diff --git a/pkg/surrogate/providers/common.go b/pkg/surrogate/providers/common.go index 01f39f59a..490047ef9 100644 --- a/pkg/surrogate/providers/common.go +++ b/pkg/surrogate/providers/common.go @@ -41,7 +41,12 @@ var storageToInfiniteTTLMap = map[string]time.Duration{ } func (s *baseStorage) ParseHeaders(value string) []string { - return strings.Split(value, s.parent.getHeaderSeparator()) + res := strings.Split(value, s.parent.getHeaderSeparator()) + for i, v := range res { + res[i] = strings.TrimSpace(v) + } + + return res } func getCandidateHeader(header http.Header, getCandidates func() []string) (string, string) { diff --git a/plugins/caddy/httpcache_test.go b/plugins/caddy/httpcache_test.go index c5eed1f3e..3911e13e8 100644 --- a/plugins/caddy/httpcache_test.go +++ b/plugins/caddy/httpcache_test.go @@ -602,6 +602,106 @@ func TestMustRevalidate(t *testing.T) { } } +type staleIfErrorHandler struct { + iterator int +} + +func (t *staleIfErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if t.iterator > 0 { + w.WriteHeader(http.StatusInternalServerError) + return + } + + t.iterator++ + w.Header().Set("Cache-Control", "stale-if-error=86400") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("Hello stale-if-error!")) +} + +func TestStaleIfError(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + cache { + ttl 5s + stale 5s + } + } + localhost:9080 { + route /stale-if-error { + cache + reverse_proxy localhost:9085 + } + }`, "caddyfile") + + go func() { + staleIfErrorHandler := staleIfErrorHandler{} + _ = http.ListenAndServe(":9085", &staleIfErrorHandler) + }() + time.Sleep(time.Second) + resp1, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusOK, "Hello stale-if-error!") + resp2, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusOK, "Hello stale-if-error!") + + if resp1.Header.Get("Cache-Control") != "stale-if-error=86400" { + 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-/stale-if-error" { + 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") != "stale-if-error=86400" { + 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-/stale-if-error; detail=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")) + } + + time.Sleep(6 * time.Second) + staleReq, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/stale-if-error", nil) + staleReq.Header = http.Header{"Cache-Control": []string{"stale-if-error=86400"}} + resp3, _ := tester.AssertResponse(staleReq, http.StatusOK, "Hello stale-if-error!") + + if resp3.Header.Get("Cache-Control") != "stale-if-error=86400" { + 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-/stale-if-error; detail=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/stale-if-error`, http.StatusOK, "Hello stale-if-error!") + + if resp4.Header.Get("Cache-Status") != "Souin; hit; ttl=-2; key=GET-http-localhost:9080-/stale-if-error; detail=DEFAULT; fwd=stale; fwd-status=500" && + resp4.Header.Get("Cache-Status") != "Souin; hit; ttl=-3; key=GET-http-localhost:9080-/stale-if-error; detail=DEFAULT; fwd=stale; fwd-status=500" { + t.Errorf("unexpected resp4 Cache-Status header %v", resp4.Header.Get("Cache-Status")) + } + + if resp4.Header.Get("Age") != "7" && resp4.Header.Get("Age") != "8" { + t.Errorf("unexpected resp4 Age header %v", resp4.Header.Get("Age")) + } + + time.Sleep(6 * time.Second) + resp5, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusInternalServerError, "") + + if resp5.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; key=GET-http-localhost:9080-/stale-if-error; detail=UNCACHEABLE-STATUS-CODE" { + t.Errorf("unexpected resp5 Cache-Status header %v", resp5.Header.Get("Cache-Status")) + } + + if resp5.Header.Get("Age") != "" { + t.Errorf("unexpected resp5 Age header %v", resp5.Header.Get("Age")) + } +} + type testETagsHandler struct{} const etagValue = "AAA-BBB"