mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
This commit is contained in:
committed by
GitHub
parent
a2df195d5e
commit
57ac712772
1
changelog.d/features/6737-vary-accept-encoding.md
Normal file
1
changelog.d/features/6737-vary-accept-encoding.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(api):** add `Vary: Accept-Encoding` to token-authenticated `/v1*`/`/v1beta*` responses so downstream caches distinguish compressed vs uncompressed variants (RFC 9110 §12.5.5). (thanks @chirag127)
|
||||
@@ -23,7 +23,11 @@ in this order:
|
||||
|
||||
1. **`CORS_ALLOW_ALL=true`** (or the legacy `CORS_ORIGIN=*`) → echo the caller's
|
||||
`Origin` back (or `*` when there is no `Origin` header), with `Vary: Origin`
|
||||
so caches stay correct.
|
||||
so caches stay correct. The same `applyCorsHeaders()` chokepoint also appends
|
||||
`Vary: Accept-Encoding` to every 2xx-with-body response on the token-authenticated
|
||||
`/v1*`/`/v1beta*` surface (`relaxForTokenAuth`, RFC 9110 §12.5.5, issue #6737), so
|
||||
downstream/shared caches can correctly distinguish compressed vs uncompressed
|
||||
variants.
|
||||
2. Otherwise, the request `Origin` is normalized (lower-cased, trailing slash
|
||||
stripped) and matched against the **merged allowlist**:
|
||||
- env **`CORS_ALLOWED_ORIGINS`** — comma-separated list, and
|
||||
|
||||
@@ -138,6 +138,11 @@ export function getCorsStatus(): CorsStatus {
|
||||
* is returned when there is no `Origin` header. This is NEVER paired with
|
||||
* `Access-Control-Allow-Credentials` (these routes are not cookie-authed), so
|
||||
* the echo/wildcard stays safe.
|
||||
*
|
||||
* On that same `relaxForTokenAuth` surface, also appends `Vary: Accept-Encoding`
|
||||
* to every response with a body (RFC 9110 §12.5.5, issue #6737) — Next's built-in
|
||||
* compression middleware only appends it conditionally, so shared caches can't
|
||||
* otherwise reliably tell compressed vs uncompressed variants apart.
|
||||
*/
|
||||
export function applyCorsHeaders(
|
||||
response: Response,
|
||||
@@ -153,6 +158,16 @@ export function applyCorsHeaders(
|
||||
response.headers.set("Access-Control-Allow-Origin", allowed);
|
||||
response.headers.append("Vary", "Origin");
|
||||
}
|
||||
// RFC 9110 §12.5.5 (issue #6737): the token-authenticated /v1*/v1beta* surface
|
||||
// (relaxForTokenAuth) negotiates content-encoding via Next's built-in
|
||||
// compression middleware, which only appends `Vary: Accept-Encoding`
|
||||
// conditionally (after its own content-type/threshold filter) — so shared
|
||||
// caches (CDNs/proxies) can't reliably tell compressed vs uncompressed variants
|
||||
// apart. Stamp it explicitly here, at the same chokepoint that already appends
|
||||
// `Vary: Origin`, on every relaxed-CORS response with a body.
|
||||
if (relaxForTokenAuth && response.status !== 204) {
|
||||
response.headers.append("Vary", "Accept-Encoding");
|
||||
}
|
||||
response.headers.set("Access-Control-Allow-Methods", STANDARD_ALLOW_METHODS);
|
||||
response.headers.set("Access-Control-Allow-Headers", STANDARD_ALLOW_HEADERS);
|
||||
const requestedHeaders = request.headers.get("access-control-request-headers");
|
||||
|
||||
@@ -174,6 +174,51 @@ describe("cors/origins.applyCorsHeaders", () => {
|
||||
assert.match(res.headers.get("Vary") || "", /Origin/);
|
||||
});
|
||||
|
||||
it("CLIENT_API: appends Vary: Accept-Encoding on a 2xx relaxForTokenAuth response (#6737)", () => {
|
||||
const res = NextResponse.json({ ok: true });
|
||||
const req = new Request("https://server.example.com/api/v1/models");
|
||||
applyCorsHeaders(res, req, true);
|
||||
assert.match(res.headers.get("Vary") || "", /Accept-Encoding/);
|
||||
});
|
||||
|
||||
it("CLIENT_API: combines with Vary: Origin into a single comma-joined header (#6737)", () => {
|
||||
process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com";
|
||||
const res = NextResponse.json({ ok: true });
|
||||
const req = new Request("https://server.example.com/api/v1/models", {
|
||||
headers: { Origin: "https://app.example.com" },
|
||||
});
|
||||
applyCorsHeaders(res, req, true);
|
||||
const varyValues = res.headers.getSetCookie ? res.headers.get("Vary") : res.headers.get("Vary");
|
||||
assert.equal(varyValues, "Origin, Accept-Encoding");
|
||||
assert.equal([...res.headers.entries()].filter(([k]) => k.toLowerCase() === "vary").length, 1);
|
||||
});
|
||||
|
||||
it("MANAGEMENT: does not append Vary: Accept-Encoding (relax off) (#6737)", () => {
|
||||
const res = NextResponse.json({ ok: true });
|
||||
const req = new Request("https://server.example.com/api/keys");
|
||||
applyCorsHeaders(res, req);
|
||||
assert.doesNotMatch(res.headers.get("Vary") || "", /Accept-Encoding/);
|
||||
applyCorsHeaders(res, req, false);
|
||||
assert.doesNotMatch(res.headers.get("Vary") || "", /Accept-Encoding/);
|
||||
});
|
||||
|
||||
it("204 response: does not append Vary: Accept-Encoding even with relaxForTokenAuth (#6737)", () => {
|
||||
const res = new NextResponse(null, { status: 204 });
|
||||
const req = new Request("https://server.example.com/api/v1/models", {
|
||||
method: "OPTIONS",
|
||||
});
|
||||
applyCorsHeaders(res, req, true);
|
||||
assert.doesNotMatch(res.headers.get("Vary") || "", /Accept-Encoding/);
|
||||
});
|
||||
|
||||
it("CLIENT_API: appends Vary: Accept-Encoding even without an Origin header (#6737)", () => {
|
||||
const res = NextResponse.json({ ok: true });
|
||||
const req = new Request("https://server.example.com/api/v1/models");
|
||||
applyCorsHeaders(res, req, true);
|
||||
assert.equal(res.headers.get("Access-Control-Allow-Origin"), "*");
|
||||
assert.match(res.headers.get("Vary") || "", /Accept-Encoding/);
|
||||
});
|
||||
|
||||
it("reflects requested headers from Access-Control-Request-Headers preflight", () => {
|
||||
process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com";
|
||||
const res = NextResponse.json({ ok: true });
|
||||
|
||||
Reference in New Issue
Block a user