Skip to content

304 Not Modified leaks to unconditional clients during stale revalidation #794

@u5surf

Description

@u5surf

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.

  1. 1st request (unconditional) -> 200 OK, stored.
  2. Wait ~2s so the entry is stale (still within the stale window).
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions