res: include Access-Control-Allow-Origin when generating ETag
Include the response Access-Control-Allow-Origin header as an extra argument to the app's compiled etag function so ETags vary by CORS origin. This prevents CDN/304 + missing CORS headers from causing browser CORS errors. Backwards compatible: custom etag functions may ignore the extra arg.
Fix: ETag should vary by CORS origin
Summary
Fixes a bug where responses with the same body but different Access-Control-Allow-Origin headers produced identical ETags.
Details
res.send() now passes the response’s Access-Control-Allow-Origin value to the ETag generator so that ETags differ per origin.
This prevents caches or CDNs from serving incorrect 304 Not Modified responses that omit or mismatch CORS headers.
Changes
-
response.js – Forward
Access-Control-Allow-Originto the ETag generator. - utils.js – No functional change; already supports optional origin argument.
- etag.cors.js – Added tests to verify ETag varies by CORS origin.
Notes
- Backward compatible – custom ETag functions ignoring extra args still work.
- Tests and CI pass successfully.
I don't think that ACAO should affect ETag value. ETag is about selected representation, which I understand to be the response data and metadata such as language, encoding and type.
If you serve different ACAO headers, then I assume it is based on the request Origin header (your tests do that). Other CORS headers such as Access-Contol-Allow-Methods and Access-Control-Allow-Headers, unless hardcoded, are likely to be based on request's Access-Control-Request-Method and Access-Control-Request-Headers headers.
In HTTP semantics (RFC 9110) there is a convinient tool for dealing with exactly such cases - the Vary header. As it is described:
The "Vary" header field in a response describes what parts of a request message, aside from the method and target URI, might have influenced the origin server's process for selecting the content of this response.
When this header is present, the cache MUST use request headers listed in response's Vary as additional keys. What this means is that if you serve responses with Vary: Origin, then responses to requests with Origin: example.com and Origin: example.org are cached separately.
If you'd like to read more, here are some references:
- RFC 9110 HTTP Semantics
- section 12.5.5 Vary: https://httpwg.org/specs/rfc9110.html#rfc.section.12.5.5
- section 15.4.5 304 Not Modified: https://httpwg.org/specs/rfc9110.html#rfc.section.15.4.5
- RFC 9111 HTTP Caching
- section 4.1 Calculating Cache Keys with the Vary Header Field https://httpwg.org/specs/rfc9111.html#caching.negotiated.responses
- Fastly blog
- Andrew Bets, Understanding the Vary header in the browser: https://www.fastly.com/blog/understanding-vary-header-browser
- Roger Mulhuijzen, Best practices for using the Vary header: https://www.fastly.com/blog/best-practices-using-vary-header