Description
When a stale response is revalidated, Souin can return 304 Not Modified to a
client that sent no conditional headers (If-None-Match / If-Modified-Since).
Per RFC 9110, 304 is only a valid response to a conditional request.
Originally reported against cache-handler:
caddyserver/cache-handler#137
Reproduction
Reproduced on master (7029609).
Backend returns Cache-Control: max-age=1, must-revalidate + an ETag, and
replies 304 when If-None-Match matches (file_server-like). Cache: ttl 1s,
stale 3600s.
- 1st request (unconditional) ->
200 OK, stored.
- Wait ~2s so the entry is stale (still within the stale window).
- 2nd identical request with
Cache-Control: max-stale=6, no conditional
headers -> 304 Not Modified (expected: 200 OK).
Response header on step 3:
Cache-Status: Souin; fwd=request; fwd-status=304; key=...; detail=REQUEST-REVALIDATION
Note: a forced revalidation is required to hit this path (must-revalidate,
no-cache, or a client conditional header). max-age alone does not reproduce
it — the stale response is then served correctly as 200.
Root cause
During stale revalidation, Souin injects the stored ETag into the upstream
request:
pkg/middleware/middleware.go:987
req.Header["If-None-Match"] = append(req.Header["If-None-Match"], validator.ResponseETag)
The upstream therefore answers 304. The block meant to rebuild the full 200
response only runs when !validator.Matched:
pkg/middleware/middleware.go:1014
if statusCode == http.StatusNotModified {
if !validator.Matched {
// rebuild full 200 from cache
}
}
But for an unconditional client request, Matched is true
(storages/core/revalidator.go:27):
validator.Matched = validator.ResponseETag == "" ||
(validator.ResponseETag != "" && len(validator.RequestETags) == 0)
So the rebuild is skipped and the internally-triggered 304 is sent to the
client (middleware.go:1038).
Expected
304 should be propagated to the client only when the client itself sent a
matching conditional request. For unconditional requests, the full cached body
should be returned with 200.
Environment
Description
When a stale response is revalidated, Souin can return
304 Not Modifiedto aclient that sent no conditional headers (
If-None-Match/If-Modified-Since).Per RFC 9110,
304is only a valid response to a conditional request.Originally reported against cache-handler:
caddyserver/cache-handler#137
Reproduction
Reproduced on master (
7029609).Backend returns
Cache-Control: max-age=1, must-revalidate+ anETag, andreplies
304whenIf-None-Matchmatches (file_server-like). Cache:ttl 1s,stale 3600s.200 OK, stored.Cache-Control: max-stale=6, no conditionalheaders ->
304 Not Modified(expected:200 OK).Response header on step 3:
Cache-Status: Souin; fwd=request; fwd-status=304; key=...; detail=REQUEST-REVALIDATIONNote: a forced revalidation is required to hit this path (
must-revalidate,no-cache, or a client conditional header).max-agealone does not reproduceit — the stale response is then served correctly as
200.Root cause
During stale revalidation, Souin injects the stored ETag into the upstream
request:
pkg/middleware/middleware.go:987The upstream therefore answers
304. The block meant to rebuild the full200response only runs when
!validator.Matched:pkg/middleware/middleware.go:1014But for an unconditional client request,
Matchedistrue(
storages/core/revalidator.go:27):So the rebuild is skipped and the internally-triggered
304is sent to theclient (
middleware.go:1038).Expected
304should be propagated to the client only when the client itself sent amatching conditional request. For unconditional requests, the full cached body
should be returned with
200.Environment
7029609)304 Not Modifiedfor unconditional request caddyserver/cache-handler#137