HttpCache::isNotModified() answers 304 whenever the offered validator exists anywhere in the ETag pool. It is not scoped to the resource being requested.
$about = $resource->get('page://self/help/about'); // ETag "927897379"
$privacy = $resource->get('page://self/help/privacy'); // ETag "3259201453"
// asking for /help/privacy while offering /help/about's validator
$httpCache->isNotModified(['HTTP_IF_NONE_MATCH' => '"927897379"', 'REQUEST_URI' => '/help/privacy']);
// => true (304)
The client is told its copy of /help/privacy is current when the server has never served that copy. RFC 9110 §13.1.2 scopes If-None-Match to the selected representation of the target resource.
ResourceStorage::hasEtag() has been a set-membership test since 2018 (645f9a7), and the ETag pool stores the validator as its key with no URI beside it.
Reachability
A client that returns the validator it was given cannot reach this: EtagSetter derives the value from $ro::class . $etag . $ro->uri, so each URI has its own. What reaches it is a client or intermediary that mixes validators across URIs, or one that offers a value it did not obtain from this server (the space is a crc32).
Why the obvious fix is worse
Storing the URI in the entry and comparing it with REQUEST_URI breaks every routed application: BeMart maps /products/5 to page://self/product?id=5, so a legitimate revalidation would compare paths that do not match and answer 200 forever.
The decision is made in Bootstrap before routing precisely so a 304 costs no resource run, and that is where the resource URI does not exist yet. A correct fix needs the routed URI at the decision point - HttpCacheInterface::isNotModified(array $server) is bear/sunday's contract - and then hasEtag() can be scoped to it.
Filing here because the pool and the decision live in this package. Recorded in an application's cache oracle as a known defect so it reports until it closes.
HttpCache::isNotModified()answers 304 whenever the offered validator exists anywhere in the ETag pool. It is not scoped to the resource being requested.The client is told its copy of
/help/privacyis current when the server has never served that copy. RFC 9110 §13.1.2 scopes If-None-Match to the selected representation of the target resource.ResourceStorage::hasEtag()has been a set-membership test since 2018 (645f9a7), and the ETag pool stores the validator as its key with no URI beside it.Reachability
A client that returns the validator it was given cannot reach this:
EtagSetterderives the value from$ro::class . $etag . $ro->uri, so each URI has its own. What reaches it is a client or intermediary that mixes validators across URIs, or one that offers a value it did not obtain from this server (the space is a crc32).Why the obvious fix is worse
Storing the URI in the entry and comparing it with
REQUEST_URIbreaks every routed application: BeMart maps/products/5topage://self/product?id=5, so a legitimate revalidation would compare paths that do not match and answer 200 forever.The decision is made in
Bootstrapbefore routing precisely so a 304 costs no resource run, and that is where the resource URI does not exist yet. A correct fix needs the routed URI at the decision point -HttpCacheInterface::isNotModified(array $server)is bear/sunday's contract - and thenhasEtag()can be scoped to it.Filing here because the pool and the decision live in this package. Recorded in an application's cache oracle as a known defect so it reports until it closes.