mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
ef003c0ebe
* fix(server): implement q-value-aware Accept-Encoding negotiation and add tests * docs(server): update comments for Accept-Encoding parsing to clarify q-value handling * fix(server): enhance q-value handling in Accept-Encoding parsing and add tests * fix(server): enhance q-value handling in Accept-Encoding parsing and add tests for isEncodingAccepted * fix(tests): update negotiateEncoding tests to reflect zstd as highest preference in prod-server.ts * fix(tests): update negotiateEncoding tests * fix(tests): remove redundant test for explicit refusal overriding wildcard in negotiateEncoding * fix(server): normalize encoding checks in isEncodingAccepted function * refactor(server): extract accept-encoding helpers * fix(tests): remove redundant zlib check and import HAS_ZSTD from accept-encoding module * fix(server): honor Accept-Encoding quality weights * fix(server): repair accept-encoding CI checks * fix(server): harden content encoding negotiation * fix(server): align encoding negotiation with Next.js * refactor(server): remove unused encoding parse state * fix(server): normalize empty vary headers --------- Co-authored-by: James <james@eli.cx>
222 lines
9.2 KiB
TypeScript
222 lines
9.2 KiB
TypeScript
/**
|
|
* Tests for RFC 9110 Accept-Encoding negotiation.
|
|
*
|
|
* Regression coverage for cloudflare/vinext#1981: negotiation must honor
|
|
* numeric client preferences, exact coding tokens, wildcards, and explicit
|
|
* refusals rather than relying on substring checks.
|
|
*/
|
|
import { describe, expect, it } from "vite-plus/test";
|
|
import type { IncomingMessage } from "node:http";
|
|
import {
|
|
getEncodingQuality,
|
|
HAS_ZSTD,
|
|
isEncodingAccepted,
|
|
negotiateEncoding,
|
|
parseAcceptedEncodings,
|
|
selectAcceptedEncoding,
|
|
selectContentEncoding,
|
|
} from "../packages/vinext/src/server/accept-encoding.js";
|
|
|
|
function reqWith(acceptEncoding?: string): IncomingMessage {
|
|
const headers: Record<string, string> = {};
|
|
if (acceptEncoding !== undefined) headers["accept-encoding"] = acceptEncoding;
|
|
return { headers } as unknown as IncomingMessage;
|
|
}
|
|
|
|
describe("parseAcceptedEncodings", () => {
|
|
it("retains numeric qualities for exact coding tokens", () => {
|
|
const parsed = parseAcceptedEncodings("gzip;q=0.9, br;q=0.1");
|
|
expect(getEncodingQuality(parsed, "gzip")).toBe(0.9);
|
|
expect(getEncodingQuality(parsed, "br")).toBe(0.1);
|
|
});
|
|
|
|
it("defaults missing q parameters to one", () => {
|
|
const parsed = parseAcceptedEncodings("gzip, br;level=4");
|
|
expect(getEncodingQuality(parsed, "gzip")).toBe(1);
|
|
expect(getEncodingQuality(parsed, "br")).toBe(1);
|
|
});
|
|
|
|
it("accepts trailing-dot q-values", () => {
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip;q=1."), "gzip")).toBe(1);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("br;q=0."), "br")).toBe(0);
|
|
});
|
|
|
|
it("accepts numeric q-values using Next.js parser semantics", () => {
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip;q=0.123"), "gzip")).toBe(0.123);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip;q=1.000"), "gzip")).toBe(1);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip;q=.5"), "gzip")).toBe(0.5);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip;q=0.1234"), "gzip")).toBe(0.1234);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip;q=1.5"), "gzip")).toBe(1.5);
|
|
});
|
|
|
|
it("drops q-values that do not start with a number", () => {
|
|
for (const value of ["abc", ""] as const) {
|
|
expect(getEncodingQuality(parseAcceptedEncodings(`gzip;q=${value}`), "gzip")).toBe(0);
|
|
}
|
|
});
|
|
|
|
it("matches Next.js handling of bare q and spaced parameter names", () => {
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip;q"), "gzip")).toBe(0);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip; q = 0.2"), "gzip")).toBe(1);
|
|
});
|
|
|
|
it("matches coding tokens exactly and case-insensitively", () => {
|
|
const parsed = parseAcceptedEncodings("GZIP, brotli-future");
|
|
expect(isEncodingAccepted(parsed, "gzip")).toBe(true);
|
|
expect(isEncodingAccepted(parsed, "br")).toBe(false);
|
|
expect(isEncodingAccepted(parsed, "brotli-future")).toBe(true);
|
|
});
|
|
|
|
it("matches Next.js parameter-name casing semantics", () => {
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip;Q=0"), "gzip")).toBe(1);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("gzip;q=0"), "gzip")).toBe(0);
|
|
});
|
|
|
|
it("finds q among other parameters and uses the first repeated q parameter", () => {
|
|
expect(getEncodingQuality(parseAcceptedEncodings("br;foo=bar;q=0"), "br")).toBe(0);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("br;q=0;foo=bar"), "br")).toBe(0);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("br;q=0;q=0.5"), "br")).toBe(0);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("br;q=0.5;q=0"), "br")).toBe(0.5);
|
|
});
|
|
|
|
it("uses the highest quality across duplicate coding entries", () => {
|
|
const parsed = parseAcceptedEncodings("gzip;q=0, gzip;q=0.7, gzip;q=0.4");
|
|
expect(getEncodingQuality(parsed, "gzip")).toBe(0.7);
|
|
});
|
|
|
|
it("applies wildcard quality only to codings without an explicit entry", () => {
|
|
const parsed = parseAcceptedEncodings("*;q=0.7, br;q=0");
|
|
expect(getEncodingQuality(parsed, "gzip")).toBe(0.7);
|
|
expect(getEncodingQuality(parsed, "br")).toBe(0);
|
|
});
|
|
|
|
it("does not apply positive wildcard quality to identity", () => {
|
|
expect(getEncodingQuality(parseAcceptedEncodings("*;q=0"), "identity")).toBe(0);
|
|
expect(getEncodingQuality(parseAcceptedEncodings("*;q=0.4"), "identity")).toBe(1);
|
|
});
|
|
|
|
it("gives implicit identity quality one without a wildcard", () => {
|
|
const parsed = parseAcceptedEncodings("gzip;q=0.8, br;q=0.2");
|
|
expect(getEncodingQuality(parsed, "identity")).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("selectAcceptedEncoding", () => {
|
|
it("selects the highest client quality before server preference", () => {
|
|
const parsed = parseAcceptedEncodings("gzip;q=1, br;q=0.1");
|
|
expect(selectAcceptedEncoding(parsed, ["br", "gzip", "identity"])).toBe("gzip");
|
|
});
|
|
|
|
it("uses available order as the tie-breaker", () => {
|
|
const parsed = parseAcceptedEncodings("gzip;q=0.5, br;q=0.5, identity;q=0.5");
|
|
expect(selectAcceptedEncoding(parsed, ["br", "gzip", "identity"])).toBe("br");
|
|
});
|
|
|
|
it("returns null when every available representation is refused", () => {
|
|
const parsed = parseAcceptedEncodings("*;q=0");
|
|
expect(selectAcceptedEncoding(parsed, ["br", "gzip", "identity"])).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe("selectContentEncoding", () => {
|
|
it("uses implicit identity as a fallback, matching Next.js compression", () => {
|
|
const parsed = parseAcceptedEncodings("gzip;q=0.5");
|
|
expect(selectContentEncoding(parsed, ["br", "gzip"])).toBe("gzip");
|
|
});
|
|
|
|
it("allows an explicit higher identity preference to win", () => {
|
|
const parsed = parseAcceptedEncodings("gzip;q=0.2, identity;q=0.8");
|
|
expect(selectContentEncoding(parsed, ["gzip"])).toBe("identity");
|
|
});
|
|
|
|
it("uses client header order for equal explicit qualities", () => {
|
|
expect(
|
|
selectContentEncoding(parseAcceptedEncodings("identity;q=0.5, gzip;q=0.5"), ["gzip"]),
|
|
).toBe("identity");
|
|
expect(
|
|
selectContentEncoding(parseAcceptedEncodings("gzip;q=0.5, identity;q=0.5"), ["gzip"]),
|
|
).toBe("gzip");
|
|
});
|
|
|
|
it("prefers explicit identity over an equal-quality wildcard", () => {
|
|
expect(selectContentEncoding(parseAcceptedEncodings("*;q=0.5, identity;q=0.5"), ["br"])).toBe(
|
|
"identity",
|
|
);
|
|
});
|
|
|
|
it("uses the later equal-quality duplicate for identity tie ordering", () => {
|
|
expect(
|
|
selectContentEncoding(parseAcceptedEncodings("gzip;q=0.8, identity;q=0.8, gzip;q=0.8"), [
|
|
"gzip",
|
|
]),
|
|
).toBe("identity");
|
|
});
|
|
|
|
it("lets later invalid duplicate qualities reject an earlier valid entry", () => {
|
|
expect(selectContentEncoding(parseAcceptedEncodings("gzip, gzip;q"), ["gzip"])).toBe(
|
|
"identity",
|
|
);
|
|
expect(selectContentEncoding(parseAcceptedEncodings("gzip;q=0.8, gzip;q=abc"), ["gzip"])).toBe(
|
|
"identity",
|
|
);
|
|
expect(
|
|
selectContentEncoding(parseAcceptedEncodings("identity, identity;q, gzip;q=0.5"), ["gzip"]),
|
|
).toBe("gzip");
|
|
});
|
|
|
|
it("applies duplicate-invalid replacement semantics to wildcard entries", () => {
|
|
expect(selectContentEncoding(parseAcceptedEncodings("*, *;q"), ["br"])).toBe("identity");
|
|
expect(selectContentEncoding(parseAcceptedEncodings("*;q, *"), ["br"])).toBe("br");
|
|
expect(selectContentEncoding(parseAcceptedEncodings("*;q=0.8, *;q"), ["br"])).toBe("identity");
|
|
expect(selectContentEncoding(parseAcceptedEncodings("*;q=abc, *;q=0.8"), ["br"])).toBe("br");
|
|
});
|
|
|
|
it("falls back to identity when no content coding is acceptable", () => {
|
|
expect(selectContentEncoding(parseAcceptedEncodings("*;q=0"), ["br", "gzip"])).toBe("identity");
|
|
});
|
|
});
|
|
|
|
describe("negotiateEncoding", () => {
|
|
it("uses identity when no Accept-Encoding header is present", () => {
|
|
expect(negotiateEncoding(reqWith(undefined))).toBe("identity");
|
|
});
|
|
|
|
it("honors unequal positive client preferences", () => {
|
|
expect(negotiateEncoding(reqWith("gzip;q=1, br;q=0.1"))).toBe("gzip");
|
|
expect(negotiateEncoding(reqWith("br;q=0.5, gzip;q=0.9"))).toBe("gzip");
|
|
});
|
|
|
|
it("uses server preference when client qualities tie", () => {
|
|
expect(negotiateEncoding(reqWith("gzip;q=0.5, br;q=0.5, identity;q=0.5"))).toBe("br");
|
|
});
|
|
|
|
it("honors explicit refusals and exact matching", () => {
|
|
expect(negotiateEncoding(reqWith("gzip, br;q=0"))).toBe("gzip");
|
|
expect(negotiateEncoding(reqWith("brotli-future"))).toBe("identity");
|
|
});
|
|
|
|
it("honors wildcard quality and explicit overrides", () => {
|
|
expect(negotiateEncoding(reqWith("*"))).toBe(HAS_ZSTD ? "zstd" : "br");
|
|
expect(negotiateEncoding(reqWith("*;q=0.8, br;q=0"))).toBe(HAS_ZSTD ? "zstd" : "gzip");
|
|
expect(negotiateEncoding(reqWith("*;q=0, gzip"))).toBe("gzip");
|
|
});
|
|
|
|
it("returns identity when it is the preferred representation", () => {
|
|
expect(negotiateEncoding(reqWith("gzip;q=0.2, identity;q=0.8"))).toBe("identity");
|
|
});
|
|
|
|
it("falls back to identity when every supported coding is refused", () => {
|
|
expect(negotiateEncoding(reqWith("*;q=0"))).toBe("identity");
|
|
expect(negotiateEncoding(reqWith("gzip;q=0, br;q=0, deflate;q=0, identity;q=0"))).toBe(
|
|
"identity",
|
|
);
|
|
});
|
|
|
|
if (HAS_ZSTD) {
|
|
it("includes zstd in quality negotiation when supported", () => {
|
|
expect(negotiateEncoding(reqWith("zstd;q=0.2, gzip;q=0.9, identity;q=0"))).toBe("gzip");
|
|
expect(negotiateEncoding(reqWith("zstd;q=0.9, br;q=0.8, identity;q=0"))).toBe("zstd");
|
|
});
|
|
}
|
|
});
|