Files
cloudflare__vinext/tests/pages-router-concurrency.test.ts
T
James Anderson 71dbb45ca8 fix(pages): isolate on-demand revalidation requests (#2495)
* fix(pages): pin revalidate loopback origin

* test(pages): cover production revalidation origin

* fix(pages): preserve internal revalidation boundaries

* fix(pages): preserve on-demand revalidation boundaries

* fix(pages): dispatch Worker revalidation internally

* fix(pages): authenticate revalidation request context

* fix(pages): isolate revalidation transport headers

* test(pages): account for generated route table size

* fix(pages): align on-demand revalidation semantics

* fix(pages): align revalidation response cache parity

* fix(pages): preserve regenerated ISR representations

* fix(pages): align ISR cache representations

* fix(pages): preserve canonical ISR representations

* fix(pages): align cached response parity

* fix(pages): preserve custom App render props

* fix(pages): preserve optional App page props

* fix(pages): preserve optional App props on the client

* fix(pages): normalize client App page props

* fix(pages): preserve App data merge semantics

* fix(pages): keep redirect status helper internal

* fix(pages): match Next.js terminal ISR behavior

* fix(pages): preserve custom app error envelopes

* fix(pages): match Next.js dev revalidation semantics

* test(pages): align dev revalidation parity coverage

* chore(pages): remove stale cache helper exports

* fix(cache): preserve explicit no-store context
2026-07-20 18:45:06 +01:00

195 lines
6.8 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from "vite-plus/test";
import { build, type ViteDevServer } from "vite";
import path from "node:path";
import http from "node:http";
import vinext from "../packages/vinext/src/index.js";
import { createIsolatedFixture, PAGES_FIXTURE_DIR, startFixtureServer } from "./helpers.js";
const CONCURRENCY = 15;
async function fetchConcurrentPages(
baseUrl: string,
pagePath: string,
count: number,
): Promise<string[]> {
const requests = Array.from({ length: count }, (_, i) =>
fetch(`${baseUrl}${pagePath}?id=${i}`).then((r) => r.text()),
);
return Promise.all(requests);
}
function extractTestId(html: string, testId: string): string | null {
const pattern = new RegExp(`data-testid="${testId}"[^>]*>([^<]*)<`);
const match = html.match(pattern);
return match?.[1] ?? null;
}
function extractTitle(html: string): string | null {
const match = html.match(/<title[^>]*>([^<]*)<\/title>/);
return match?.[1] ?? null;
}
function extractMetaContent(html: string, name: string): string | null {
const pattern = new RegExp(`<meta(?=[^>]*name="${name}")[^>]+content="([^"]*)"`, "i");
const match = html.match(pattern);
return match?.[1] ?? null;
}
describe("Pages Router dev concurrency isolation", () => {
let server: ViteDevServer;
let baseUrl: string;
beforeAll(async () => {
({ server, baseUrl } = await startFixtureServer(PAGES_FIXTURE_DIR));
}, 30000);
afterAll(async () => {
await server?.close();
});
it("<Head> elements do not leak between concurrent requests", async () => {
const htmlResults = await fetchConcurrentPages(baseUrl, "/concurrent-head", CONCURRENCY);
for (let i = 0; i < CONCURRENCY; i++) {
const title = extractTitle(htmlResults[i]);
const metaReqId = extractMetaContent(htmlResults[i], "req-id");
const bodyReqId = extractTestId(htmlResults[i], "req-id");
expect(bodyReqId, `response ${i} should contain its own req-id`).toBe(String(i));
expect(title, `response ${i} should have its own <title>`).toBe(`req-${bodyReqId}`);
expect(metaReqId, `response ${i} meta req-id should match body`).toBe(bodyReqId);
}
});
it("router SSR context does not leak between concurrent requests", async () => {
const htmlResults = await fetchConcurrentPages(baseUrl, "/concurrent-router", CONCURRENCY);
for (let i = 0; i < CONCURRENCY; i++) {
const ssrPathname = extractTestId(htmlResults[i], "ssr-pathname");
const routerPathname = extractTestId(htmlResults[i], "router-pathname");
expect(ssrPathname, `response ${i} ssr-pathname`).toBe("/concurrent-router");
expect(routerPathname, `response ${i} router-pathname`).toBe("/concurrent-router");
const nextData = htmlResults[i].match(
/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
);
if (!nextData) throw new Error(`no __NEXT_DATA__ found in response ${i}`);
const data = JSON.parse(nextData[1]);
expect(data.props.pageProps.ssrQuery.id, `response ${i} ssrQuery.id`).toBe(String(i));
}
});
});
describe("Pages Router prod concurrency isolation", () => {
let prodServer: http.Server | undefined;
let prodPort: number;
let tmpDir: string;
beforeAll(async () => {
tmpDir = await createIsolatedFixture(
PAGES_FIXTURE_DIR,
"vinext-pages-concurrency-",
(src) => !src.includes("node_modules") && !src.includes("alias-test"),
);
const outDir = path.join(tmpDir, "dist");
await build({
root: tmpDir,
configFile: false,
plugins: [vinext()],
logLevel: "silent",
// This isolated build deliberately skips the workspace Vite config;
// retain its source alias so fixture imports of exported vinext internals
// resolve without requiring a prebuilt package.
resolve: {
alias: {
"vinext/internal": path.resolve(import.meta.dirname, "../packages/vinext/src"),
},
},
build: {
outDir: path.join(outDir, "server"),
ssr: "virtual:vinext-server-entry",
rolldownOptions: { output: { entryFileNames: "entry.js" } },
},
});
await build({
root: tmpDir,
configFile: false,
plugins: [vinext()],
logLevel: "silent",
build: {
outDir: path.join(outDir, "client"),
manifest: true,
ssrManifest: true,
rolldownOptions: { input: "virtual:vinext-client-entry" },
},
});
const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
({ server: prodServer } = await startProdServer({
port: 0,
host: "127.0.0.1",
outDir,
}));
const addr = prodServer!.address();
if (!addr || typeof addr === "string") {
throw new Error("Failed to start production server");
}
prodPort = addr.port;
}, 60000);
afterAll(async () => {
if (prodServer) {
await new Promise<void>((resolve) => prodServer!.close(() => resolve()));
}
if (tmpDir) {
const fsp = await import("node:fs/promises");
await fsp.rm(tmpDir, { recursive: true, force: true });
}
});
// prod does not inject <Head> children into the html <head> section, so we
// can only verify getServerSideProps data isolation here (not title/meta).
it("getServerSideProps data does not leak between concurrent requests", async () => {
const base = `http://127.0.0.1:${prodPort}`;
const htmlResults = await fetchConcurrentPages(base, "/concurrent-head", CONCURRENCY);
for (let i = 0; i < CONCURRENCY; i++) {
const bodyReqId = extractTestId(htmlResults[i], "req-id");
expect(bodyReqId, `response ${i} should contain its own req-id`).toBe(String(i));
const nextData = htmlResults[i].match(
/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
);
if (!nextData) throw new Error(`no __NEXT_DATA__ found in response ${i}`);
const data = JSON.parse(nextData[1]);
expect(data.props.pageProps.reqId, `response ${i} pageProps.reqId`).toBe(String(i));
}
});
it("SSR props do not leak between concurrent requests", async () => {
const base = `http://127.0.0.1:${prodPort}`;
const htmlResults = await fetchConcurrentPages(base, "/concurrent-router", CONCURRENCY);
for (let i = 0; i < CONCURRENCY; i++) {
const ssrPathname = extractTestId(htmlResults[i], "ssr-pathname");
expect(ssrPathname, `response ${i} ssr-pathname`).toBe("/concurrent-router");
const nextData = htmlResults[i].match(
/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
);
if (!nextData) throw new Error(`no __NEXT_DATA__ found in response ${i}`);
const data = JSON.parse(nextData[1]);
expect(data.props.pageProps.ssrQuery.id, `response ${i} ssrQuery.id`).toBe(String(i));
}
});
});