envoyproxy / envoyproxy/envoy

cache/cache_v2: Responses without a Date header are always treated as stale

Open
#46,947 1 comment 0 reactions 0 assignees View on GitHub
area/cache bug
Dominant language
C++
Stars
28.9k
Forks
5.6k
Avg merge
1d 20h
Merged PRs (30d)
437

Description

Responses with max-age but no Date are revalidated or refetched on every request.

[RFC 9110 6.6.1 Date](https://www.rfc-editor.org/info/rfc9110/#section-6.6.1)
```
A recipient with a clock that receives a response message without a Date header field MUST record
the time it was received and append a corresponding Date header field to the message's header
section if it is cached or forwarded downstream.
```

Envoy already does this in [ConnectionManagerImpl::ActiveStream::encodeHeaders](https://github.com/envoyproxy/envoy/blob/v1.39.0/source/common/http/conn_manager_impl.cc#L1923), but that runs after the filter chain.

Envoy determines cacheability in [CacheabilityUtils::isCacheableResponse](https://github.com/envoyproxy/envoy/blob/v1.39.0/source/extensions/filters/http/cache_v2/cacheability_utils.cc#L75), either `no-cache`, `max-age or s-maxage`, or `Expires and Date`.

If a response without a Date header with `max-age or s-maxage` reaches either cache or cache_v2, [CacheHeadersUtils::httpTime](https://github.com/envoyproxy/envoy/blob/v1.39.0/source/extensions/filters/http/cache_v2/cache_headers_utils.cc#L188) returns a default SystemTime (epoch) for a missing or unparseable Date header and [CacheHeadersUtils::calculateAge](https://github.com/envoyproxy/envoy/blob/v1.39.0/source/extensions/filters/http/cache_v2/cache_headers_utils.cc#L223) effectively calculates the apparent age as response_time's time since epoch.

[RFC 9111 4.2.1-2.3](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.1-2.3) defers to [RFC 9110 6.6.1 Date](https://www.rfc-editor.org/info/rfc9110/#section-6.6.1) for responses without Date.

A response with max-age/s-maxage without Date is stored but considered stale on every lookup.

With validators the cache fires a conditional GET, which adds origin RTT to every response.

Without validators it fires an unconditional GET and rewrites the cache entry on every request.

max-age larger than the epoch offset serves Age equal to response_time's time since epoch.

With no validators, [injectValidationHeaders](https://github.com/envoyproxy/envoy/blob/v1.39.0/source/extensions/filters/http/cache_v2/cache_headers_utils.cc#L237) falls back to Date for an If-Modified-Since conditional request, which results in a malformed upstream request (and empty If-Modified-Since).

*Repro steps*:

envoy.filters.http.cache_v2 (or cache) in front of an origin that returns 200 with Cache-Control: max-age=3600, an ETag, and no Date header.

Every request after the first reaches the origin as an conditional GET with If-None-Match, plus an empty If-Modified-Since when Last-Modified is also missing.

*Proposed fix*: Use response_time as a fallback to Date. I'll make a PR for this.

Contributor guide

Open the contributing guide

Research direction

Start with source/extensions/filters/http/cache_v2/cache_headers_utils.cc, especially httpTime, calculateAge, and injectValidationHeaders, then review cacheability_utils.cc and reproduce the cache and cache_v2 behavior described. Done means a cacheable max-age or s-maxage response without Date uses its receipt time, avoids unnecessary revalidation or refetching, and does not generate an empty If-Modified-Since header.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.