Skip to content

Commit

Permalink
fix(chore): stale-if-error
Browse files Browse the repository at this point in the history
  • Loading branch information
darkweak committed Oct 17, 2024
1 parent c195975 commit 92e0467
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 8 deletions.
30 changes: 29 additions & 1 deletion pkg/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
}

}
}
}

Expand Down
16 changes: 15 additions & 1 deletion pkg/rfc/age.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
10 changes: 5 additions & 5 deletions pkg/rfc/age_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
7 changes: 6 additions & 1 deletion pkg/surrogate/providers/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
100 changes: 100 additions & 0 deletions plugins/caddy/httpcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 92e0467

Please sign in to comment.