mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
d0839903e8
* fix(app-router): emit x-edge-runtime marker on edge RSC route response Closes #1531 The Next.js edge-runtime app template (edge-ssr-app.ts) sets `x-edge-runtime: 1` on every App Router response. vinext serves App Router responses from a Cloudflare Worker (an edge-equivalent runtime) but was not emitting this marker, so Next.js' upstream test suite (`test/e2e/app-dir/app/index.test.ts`) saw `null` for the header. Add the header to both the RSC and HTML response builders so the assertion passes for any App Router page, regardless of which payload shape the client requests. * fix(app-router): gate x-edge-runtime marker on configured runtime Only emit `x-edge-runtime: 1` when the route's resolved segment config opts into the edge runtime (`export const runtime = "edge"` or `"experimental-edge"`). Plumb the resolved runtime from the segment config through dispatch and the render lifecycle so both RSC and HTML response builders see it. Without the gate, nodejs-runtime routes also advertise the marker, which hides the configured runtime from downstream consumers.
610 lines
20 KiB
TypeScript
610 lines
20 KiB
TypeScript
import { describe, expect, it } from "vite-plus/test";
|
|
import {
|
|
buildAppPageHtmlResponse,
|
|
buildAppPageRscResponse,
|
|
mergeMiddlewareResponseHeaders,
|
|
resolveAppPageHtmlResponsePolicy,
|
|
resolveAppPageRscResponsePolicy,
|
|
} from "../packages/vinext/src/server/app-page-response.js";
|
|
import {
|
|
VINEXT_RSC_COMPATIBILITY_ID_HEADER,
|
|
VINEXT_RSC_VARY_HEADER,
|
|
} from "../packages/vinext/src/server/app-rsc-cache-busting.js";
|
|
import { withEnvVar } from "./env-test-helpers.js";
|
|
|
|
function createBody(text: string): ReadableStream {
|
|
return new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode(text));
|
|
controller.close();
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("app page response helpers", () => {
|
|
it("resolves RSC response policy for static and ISR responses", () => {
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: true,
|
|
isProduction: true,
|
|
revalidateSeconds: null,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "s-maxage=31536000, stale-while-revalidate",
|
|
cacheState: "STATIC",
|
|
});
|
|
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: false,
|
|
expireSeconds: 300,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "s-maxage=60, stale-while-revalidate=240",
|
|
cacheState: "MISS",
|
|
});
|
|
});
|
|
|
|
it("resolves RSC response policy for force-dynamic, infinity, and default cases", () => {
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: true,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
});
|
|
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: Infinity,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "s-maxage=31536000, stale-while-revalidate",
|
|
cacheState: "STATIC",
|
|
});
|
|
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: null,
|
|
}),
|
|
).toEqual({});
|
|
});
|
|
|
|
it("resolves RSC response policy as no-store when dynamic usage is detected during build", () => {
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: true,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
});
|
|
});
|
|
|
|
it("resolves draft mode response policies as uncacheable", () => {
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: false,
|
|
isDraftMode: true,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
});
|
|
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: false,
|
|
hasScriptNonce: false,
|
|
isDraftMode: true,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
shouldWriteToCache: false,
|
|
});
|
|
});
|
|
|
|
it("resolves HTML response policy precedence", () => {
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: true,
|
|
hasScriptNonce: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
shouldWriteToCache: false,
|
|
});
|
|
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: false,
|
|
hasScriptNonce: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: false,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "s-maxage=60, stale-while-revalidate",
|
|
cacheState: undefined,
|
|
shouldWriteToCache: false,
|
|
});
|
|
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: false,
|
|
hasScriptNonce: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: Infinity,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "s-maxage=31536000, stale-while-revalidate",
|
|
cacheState: "STATIC",
|
|
shouldWriteToCache: false,
|
|
});
|
|
});
|
|
|
|
it("resolves HTML response policy when cache writes stay enabled", () => {
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: false,
|
|
hasScriptNonce: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "s-maxage=60, stale-while-revalidate",
|
|
cacheState: "MISS",
|
|
shouldWriteToCache: true,
|
|
});
|
|
});
|
|
|
|
it("treats progressive action HTML responses as no-store", () => {
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: false,
|
|
isProgressiveActionRender: true,
|
|
hasScriptNonce: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
shouldWriteToCache: false,
|
|
});
|
|
});
|
|
|
|
it("treats revalidate = 0 as no-store in RSC response policy", () => {
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 0,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
});
|
|
|
|
// revalidate = 0 takes priority over isForceStatic
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: true,
|
|
isProduction: true,
|
|
revalidateSeconds: 0,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
});
|
|
});
|
|
|
|
it("treats revalidate = 0 as no-store in HTML response policy", () => {
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: false,
|
|
hasScriptNonce: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 0,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
shouldWriteToCache: false,
|
|
});
|
|
|
|
// revalidate = 0 takes priority over isForceStatic
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: false,
|
|
hasScriptNonce: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: true,
|
|
isProduction: true,
|
|
revalidateSeconds: 0,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
shouldWriteToCache: false,
|
|
});
|
|
});
|
|
|
|
it("treats force-static with explicit revalidate as ISR in both policy helpers", () => {
|
|
expect(
|
|
resolveAppPageRscResponsePolicy({
|
|
dynamicUsedDuringBuild: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: true,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "s-maxage=60, stale-while-revalidate",
|
|
cacheState: "MISS",
|
|
});
|
|
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: false,
|
|
hasScriptNonce: false,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: true,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "s-maxage=60, stale-while-revalidate",
|
|
cacheState: "MISS",
|
|
shouldWriteToCache: true,
|
|
});
|
|
});
|
|
|
|
it("treats HTML responses with a script nonce as no-store", () => {
|
|
expect(
|
|
resolveAppPageHtmlResponsePolicy({
|
|
dynamicUsedDuringRender: false,
|
|
hasScriptNonce: true,
|
|
isDraftMode: false,
|
|
isDynamicError: false,
|
|
isForceDynamic: false,
|
|
isForceStatic: false,
|
|
isProduction: true,
|
|
revalidateSeconds: 60,
|
|
}),
|
|
).toEqual({
|
|
cacheControl: "no-store, must-revalidate",
|
|
shouldWriteToCache: false,
|
|
});
|
|
});
|
|
|
|
it("builds RSC responses with params, middleware headers, and timing", async () => {
|
|
const middlewareHeaders = new Headers();
|
|
middlewareHeaders.set("cache-control", "private, max-age=5");
|
|
middlewareHeaders.append("set-cookie", "session=abc; Path=/");
|
|
middlewareHeaders.append("vary", "Next-Router-State-Tree");
|
|
|
|
const response = buildAppPageRscResponse(createBody("flight"), {
|
|
middlewareContext: {
|
|
headers: middlewareHeaders,
|
|
status: 202,
|
|
},
|
|
params: { slug: "test" },
|
|
policy: {
|
|
cacheControl: "s-maxage=60, stale-while-revalidate",
|
|
cacheState: "MISS",
|
|
},
|
|
timing: {
|
|
compileEnd: 15,
|
|
handlerStart: 10,
|
|
responseKind: "rsc",
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(202);
|
|
expect(response.headers.get("content-type")).toBe("text/x-component");
|
|
expect(response.headers.get("x-vinext-params")).toBe(encodeURIComponent('{"slug":"test"}'));
|
|
expect(response.headers.get("cache-control")).toBe("private, max-age=5");
|
|
expect(response.headers.get("x-vinext-cache")).toBe("MISS");
|
|
expect(response.headers.get("x-nextjs-cache")).toBe("MISS");
|
|
expect(response.headers.get("vary")).toBe(VINEXT_RSC_VARY_HEADER);
|
|
expect(response.headers.get("x-vinext-timing")).toBe("10,5,-1");
|
|
await expect(response.text()).resolves.toBe("flight");
|
|
});
|
|
|
|
it("emits the `x-edge-runtime: 1` marker on RSC responses when the route opts into the edge runtime (issue #1531)", () => {
|
|
// Next.js sets `x-edge-runtime: 1` on edge-runtime app responses (see
|
|
// edge-ssr-app.ts in the Next.js source). Mirror that only for routes
|
|
// whose resolved segment config is `runtime = "edge"`.
|
|
const response = buildAppPageRscResponse(createBody("flight"), {
|
|
isEdgeRuntime: true,
|
|
middlewareContext: { headers: null, status: null },
|
|
policy: {},
|
|
});
|
|
|
|
expect(response.headers.get("x-edge-runtime")).toBe("1");
|
|
});
|
|
|
|
it("omits the `x-edge-runtime` marker on RSC responses for nodejs-runtime routes", () => {
|
|
const response = buildAppPageRscResponse(createBody("flight"), {
|
|
middlewareContext: { headers: null, status: null },
|
|
policy: {},
|
|
});
|
|
|
|
expect(response.headers.get("x-edge-runtime")).toBeNull();
|
|
});
|
|
|
|
it("builds RSC responses with the current compatibility ID header", () => {
|
|
const response = withEnvVar("__VINEXT_RSC_COMPATIBILITY_ID", "compat-a", () =>
|
|
buildAppPageRscResponse(createBody("flight"), {
|
|
middlewareContext: { headers: null, status: null },
|
|
policy: {},
|
|
}),
|
|
);
|
|
|
|
expect(response.headers.get(VINEXT_RSC_COMPATIBILITY_ID_HEADER)).toBe("compat-a");
|
|
});
|
|
|
|
it("keeps the framework compatibility ID when middleware sets the internal header", () => {
|
|
const middlewareHeaders = new Headers({
|
|
[VINEXT_RSC_COMPATIBILITY_ID_HEADER]: "middleware-compat",
|
|
});
|
|
|
|
const response = withEnvVar("__VINEXT_RSC_COMPATIBILITY_ID", "framework-compat", () =>
|
|
buildAppPageRscResponse(createBody("flight"), {
|
|
middlewareContext: { headers: middlewareHeaders, status: null },
|
|
policy: {},
|
|
}),
|
|
);
|
|
|
|
expect(response.headers.get(VINEXT_RSC_COMPATIBILITY_ID_HEADER)).toBe("framework-compat");
|
|
});
|
|
|
|
it("percent-encodes X-Vinext-Params so non-ASCII characters survive the ByteString header constraint (issue #676)", () => {
|
|
// HTTP headers are ByteStrings: each character value must be <= 255.
|
|
// JSON.stringify preserves non-ASCII characters verbatim (e.g. Korean 완 = U+C644 = 50756),
|
|
// which causes Headers.set() to throw a TypeError in compliant runtimes.
|
|
// The fix: encodeURIComponent the JSON before setting the header.
|
|
const koreanSlug = "useState-완전정복";
|
|
const response = buildAppPageRscResponse(createBody("flight"), {
|
|
middlewareContext: { headers: new Headers(), status: 200 },
|
|
params: { slug: [koreanSlug] },
|
|
policy: {},
|
|
timing: { handlerStart: 0, responseKind: "rsc" },
|
|
});
|
|
|
|
const rawHeader = response.headers.get("x-vinext-params")!;
|
|
// Header value must be ASCII-safe (all byte values <= 127 after encoding)
|
|
expect(Array.from(rawHeader).every((c) => c.charCodeAt(0) <= 127)).toBe(true);
|
|
// Decoding must round-trip back to the original params
|
|
expect(JSON.parse(decodeURIComponent(rawHeader))).toEqual({ slug: [koreanSlug] });
|
|
});
|
|
|
|
it("builds HTML responses with middleware override/append header semantics", async () => {
|
|
const middlewareHeaders = new Headers();
|
|
middlewareHeaders.set("cache-control", "private, max-age=5");
|
|
middlewareHeaders.append("set-cookie", "mw=1; Path=/");
|
|
middlewareHeaders.set("vary", "Next-Router-State-Tree");
|
|
middlewareHeaders.append("x-extra", "present");
|
|
middlewareHeaders.set("cache-control", "private, max-age=5");
|
|
|
|
const response = buildAppPageHtmlResponse(createBody("<h1>page</h1>"), {
|
|
draftCookie: "__prerender_bypass=token; Path=/",
|
|
fontLinkHeader: "</font.woff2>; rel=preload; as=font; type=font/woff2; crossorigin",
|
|
middlewareContext: {
|
|
headers: middlewareHeaders,
|
|
status: 203,
|
|
},
|
|
policy: {
|
|
cacheControl: "s-maxage=31536000, stale-while-revalidate",
|
|
cacheState: "STATIC",
|
|
},
|
|
timing: {
|
|
compileEnd: 12,
|
|
handlerStart: 10,
|
|
renderEnd: 20,
|
|
responseKind: "html",
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(203);
|
|
expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8");
|
|
expect(response.headers.get("cache-control")).toBe("private, max-age=5");
|
|
expect(response.headers.get("x-vinext-cache")).toBe("STATIC");
|
|
expect(response.headers.get("x-nextjs-cache")).toBe("HIT");
|
|
expect(response.headers.get("vary")).toBe(VINEXT_RSC_VARY_HEADER);
|
|
expect(response.headers.get("link")).toBe(
|
|
"</font.woff2>; rel=preload; as=font; type=font/woff2; crossorigin",
|
|
);
|
|
expect(response.headers.get("x-extra")).toBe("present");
|
|
expect(response.headers.get("x-vinext-timing")).toBe("10,2,8");
|
|
|
|
const setCookies = response.headers.getSetCookie();
|
|
expect(setCookies).toContain("__prerender_bypass=token; Path=/");
|
|
expect(setCookies).toContain("mw=1; Path=/");
|
|
await expect(response.text()).resolves.toBe("<h1>page</h1>");
|
|
});
|
|
|
|
it("emits the `x-edge-runtime: 1` marker on HTML responses when the route opts into the edge runtime (issue #1531)", () => {
|
|
// Next.js sets `x-edge-runtime: 1` on edge-runtime app responses (see
|
|
// edge-ssr-app.ts in the Next.js source). Mirror that only for routes
|
|
// whose resolved segment config is `runtime = "edge"`.
|
|
const response = buildAppPageHtmlResponse(createBody("<h1>page</h1>"), {
|
|
isEdgeRuntime: true,
|
|
middlewareContext: { headers: null, status: null },
|
|
policy: {},
|
|
});
|
|
|
|
expect(response.headers.get("x-edge-runtime")).toBe("1");
|
|
});
|
|
|
|
it("omits the `x-edge-runtime` marker on HTML responses for nodejs-runtime routes", () => {
|
|
const response = buildAppPageHtmlResponse(createBody("<h1>page</h1>"), {
|
|
middlewareContext: { headers: null, status: null },
|
|
policy: {},
|
|
});
|
|
|
|
expect(response.headers.get("x-edge-runtime")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("mergeMiddlewareResponseHeaders", () => {
|
|
it("is a no-op when middleware headers are null", () => {
|
|
const target = new Headers({ "Content-Type": "text/plain" });
|
|
mergeMiddlewareResponseHeaders(target, null);
|
|
expect(target.get("Content-Type")).toBe("text/plain");
|
|
expect([...target].length).toBe(1);
|
|
});
|
|
|
|
it("sets singular headers via set(), overriding existing values", () => {
|
|
const target = new Headers({ "Cache-Control": "no-store", "X-Custom": "original" });
|
|
const mwHeaders = new Headers();
|
|
mwHeaders.set("Cache-Control", "private, max-age=5");
|
|
mwHeaders.set("X-Custom", "from-middleware");
|
|
|
|
mergeMiddlewareResponseHeaders(target, mwHeaders);
|
|
|
|
expect(target.get("Cache-Control")).toBe("private, max-age=5");
|
|
expect(target.get("X-Custom")).toBe("from-middleware");
|
|
});
|
|
|
|
it("appends Set-Cookie headers instead of overriding", () => {
|
|
const target = new Headers();
|
|
target.append("Set-Cookie", "existing=1; Path=/");
|
|
const mwHeaders = new Headers();
|
|
mwHeaders.append("Set-Cookie", "mw-session=abc; Path=/");
|
|
|
|
mergeMiddlewareResponseHeaders(target, mwHeaders);
|
|
|
|
const cookies = target.getSetCookie();
|
|
expect(cookies).toContain("existing=1; Path=/");
|
|
expect(cookies).toContain("mw-session=abc; Path=/");
|
|
});
|
|
|
|
it("appends Vary headers instead of overriding", () => {
|
|
const target = new Headers({ Vary: "RSC, Accept" });
|
|
const mwHeaders = new Headers();
|
|
mwHeaders.set("Vary", "Next-Router-State-Tree");
|
|
|
|
mergeMiddlewareResponseHeaders(target, mwHeaders);
|
|
|
|
expect(target.get("Vary")).toBe("RSC, Accept, Next-Router-State-Tree");
|
|
});
|
|
|
|
it("deduplicates Vary values when appending middleware headers", () => {
|
|
const target = new Headers({ Vary: VINEXT_RSC_VARY_HEADER });
|
|
const mwHeaders = new Headers();
|
|
mwHeaders.set("Vary", "next-router-state-tree, X-Auth-State");
|
|
|
|
mergeMiddlewareResponseHeaders(target, mwHeaders);
|
|
|
|
expect(target.get("Vary")).toBe(`${VINEXT_RSC_VARY_HEADER}, X-Auth-State`);
|
|
});
|
|
|
|
it("preserves wildcard Vary semantics when appending middleware headers", () => {
|
|
const target = new Headers({ Vary: "RSC, Accept" });
|
|
const mwHeaders = new Headers();
|
|
mwHeaders.set("Vary", "*, X-Auth-State");
|
|
|
|
mergeMiddlewareResponseHeaders(target, mwHeaders);
|
|
|
|
expect(target.get("Vary")).toBe("*");
|
|
});
|
|
|
|
it("preserves wildcard Vary semantics when target already has Vary wildcard", () => {
|
|
const target = new Headers({ Vary: "*" });
|
|
const mwHeaders = new Headers();
|
|
mwHeaders.set("Vary", "X-Auth-State");
|
|
|
|
mergeMiddlewareResponseHeaders(target, mwHeaders);
|
|
|
|
expect(target.get("Vary")).toBe("*");
|
|
});
|
|
|
|
it("preserves wildcard Vary semantics when middleware is the first Vary source", () => {
|
|
const target = new Headers();
|
|
const mwHeaders = new Headers();
|
|
mwHeaders.set("Vary", "*, X-Auth-State");
|
|
|
|
mergeMiddlewareResponseHeaders(target, mwHeaders);
|
|
|
|
expect(target.get("Vary")).toBe("*");
|
|
});
|
|
});
|