cache/cache_v2: Responses without a Date header are always treated as stale
- 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
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