diff --git a/packages/vinext/src/deploy.ts b/packages/vinext/src/deploy.ts index 611e247d..a8f43274 100644 --- a/packages/vinext/src/deploy.ts +++ b/packages/vinext/src/deploy.ts @@ -543,7 +543,11 @@ export default { } // Build request context for has/missing condition matching. - // Created before middleware runs, matching prod-server ordering. + // headers and redirects run before middleware, so they use this + // pre-middleware snapshot. beforeFiles, afterFiles, and fallback + // rewrites run after middleware (App Router order), so they use + // postMwReqCtx created after x-middleware-request-* headers are + // unpacked into request. const reqCtx = requestContextFromRequest(request); // ── 3. Run middleware ────────────────────────────────────────── @@ -609,6 +613,11 @@ export default { }); } + // Rebuild context after middleware has unpacked x-middleware-request-* + // headers into the cloned request. Used only for afterFiles and fallback + // rewrites, which run after middleware in the App Router execution order. + const postMwReqCtx = requestContextFromRequest(request); + let resolvedPathname = resolvedUrl.split("?")[0]; // ── 4. Apply custom headers from next.config.js ─────────────── @@ -647,7 +656,7 @@ export default { // ��─ 6. Apply beforeFiles rewrites from next.config.js ───────── if (configRewrites.beforeFiles?.length) { - const rewritten = matchRewrite(resolvedPathname, configRewrites.beforeFiles, reqCtx); + const rewritten = matchRewrite(resolvedPathname, configRewrites.beforeFiles, postMwReqCtx); if (rewritten) { if (isExternalUrl(rewritten)) { return proxyExternalRequest(request, rewritten); @@ -667,7 +676,7 @@ export default { // ── 8. Apply afterFiles rewrites from next.config.js ────────── if (configRewrites.afterFiles?.length) { - const rewritten = matchRewrite(resolvedPathname, configRewrites.afterFiles, reqCtx); + const rewritten = matchRewrite(resolvedPathname, configRewrites.afterFiles, postMwReqCtx); if (rewritten) { if (isExternalUrl(rewritten)) { return proxyExternalRequest(request, rewritten); @@ -684,7 +693,7 @@ export default { // ── 10. Fallback rewrites (if SSR returned 404) ───────────── if (response && response.status === 404 && configRewrites.fallback?.length) { - const fallbackRewrite = matchRewrite(resolvedPathname, configRewrites.fallback, reqCtx); + const fallbackRewrite = matchRewrite(resolvedPathname, configRewrites.fallback, postMwReqCtx); if (fallbackRewrite) { if (isExternalUrl(fallbackRewrite)) { return proxyExternalRequest(request, fallbackRewrite); diff --git a/packages/vinext/src/server/app-dev-server.ts b/packages/vinext/src/server/app-dev-server.ts index 67093af0..d0465ebb 100644 --- a/packages/vinext/src/server/app-dev-server.ts +++ b/packages/vinext/src/server/app-dev-server.ts @@ -213,7 +213,7 @@ import { } from "@vitejs/plugin-rsc/rsc"; import { createElement, Suspense, Fragment } from "react"; import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation"; -import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders } from "next/headers"; +import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext } from "next/headers"; import { NextRequest } from "next/server"; import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary"; import { LayoutSegmentProvider } from "vinext/layout-segment-context"; @@ -1068,6 +1068,28 @@ function __buildRequestContext(request) { }; } +/** + * Build a request context from the live ALS HeadersContext, which reflects + * any x-middleware-request-* header mutations applied by middleware. + * Used for afterFiles and fallback rewrite has/missing evaluation — these + * run after middleware in the App Router execution order. + */ +function __buildPostMwRequestContext(request) { + const url = new URL(request.url); + const ctx = getHeadersContext(); + if (!ctx) return __buildRequestContext(request); + // ctx.cookies is a Map (HeadersContext), but RequestContext + // requires a plain Record for has/missing cookie evaluation + // (config-matchers.ts uses obj[key] not Map.get()). Convert here. + const cookiesRecord = Object.fromEntries(ctx.cookies); + return { + headers: ctx.headers, + cookies: cookiesRecord, + query: url.searchParams, + host: ctx.headers.get("host") || url.host, + }; +} + function __sanitizeDestination(dest) { if (dest.startsWith("http://") || dest.startsWith("https://")) return dest; dest = dest.replace(/^[\\\\/]+/, "/"); @@ -1361,21 +1383,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { } } - // ── Apply beforeFiles rewrites from next.config.js ──────────────────── - if (__configRewrites.beforeFiles && __configRewrites.beforeFiles.length) { - // Strip .rsc suffix before matching rewrite rules — same reason as redirects above. - const __rewritePathname = pathname.endsWith(".rsc") ? pathname.slice(0, -4) : pathname; - const __rewritten = __applyConfigRewrites(__rewritePathname, __configRewrites.beforeFiles, __reqCtx); - if (__rewritten) { - if (__isExternalUrl(__rewritten)) { - setHeadersContext(null); - setNavigationContext(null); - return __proxyExternalRequest(request, __rewritten); - } - pathname = __rewritten; - } - } - const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component"); let cleanPathname = pathname.replace(/\\.rsc$/, ""); @@ -1469,6 +1476,27 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { } ` : ""} + // Build post-middleware request context for afterFiles/fallback rewrites. + // These run after middleware in the App Router execution order and should + // evaluate has/missing conditions against middleware-modified headers. + // When no middleware is present, this falls back to __buildRequestContext. + const __postMwReqCtx = __buildPostMwRequestContext(request); + + // ── Apply beforeFiles rewrites from next.config.js ──────────────────── + // In App Router execution order, beforeFiles runs after middleware so that + // has/missing conditions can evaluate against middleware-modified headers. + if (__configRewrites.beforeFiles && __configRewrites.beforeFiles.length) { + const __rewritten = __applyConfigRewrites(cleanPathname, __configRewrites.beforeFiles, __postMwReqCtx); + if (__rewritten) { + if (__isExternalUrl(__rewritten)) { + setHeadersContext(null); + setNavigationContext(null); + return __proxyExternalRequest(request, __rewritten); + } + cleanPathname = __rewritten; + } + } + // ── Image optimization passthrough (dev mode — no transformation) ─────── if (cleanPathname === "/_vinext/image") { const __rawImgUrl = url.searchParams.get("url"); @@ -1694,7 +1722,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // ── Apply afterFiles rewrites from next.config.js ────────────────────── if (__configRewrites.afterFiles && __configRewrites.afterFiles.length) { - const __afterRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.afterFiles, __reqCtx); + const __afterRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.afterFiles, __postMwReqCtx); if (__afterRewritten) { if (__isExternalUrl(__afterRewritten)) { setHeadersContext(null); @@ -1709,7 +1737,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // ── Fallback rewrites from next.config.js (if no route matched) ─────── if (!match && __configRewrites.fallback && __configRewrites.fallback.length) { - const __fallbackRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.fallback, __reqCtx); + const __fallbackRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.fallback, __postMwReqCtx); if (__fallbackRewritten) { if (__isExternalUrl(__fallbackRewritten)) { setHeadersContext(null); diff --git a/packages/vinext/src/server/prod-server.ts b/packages/vinext/src/server/prod-server.ts index c0df3d13..13dde49e 100644 --- a/packages/vinext/src/server/prod-server.ts +++ b/packages/vinext/src/server/prod-server.ts @@ -765,7 +765,10 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { duplex: hasBody ? "half" : undefined, }); - // Build request context for has/missing condition matching + // Build request context for has/missing condition matching. + // headers and redirects run before middleware and use this pre-middleware + // snapshot. beforeFiles, afterFiles, and fallback all run after middleware + // per the Next.js execution order, so they use postMwReqCtx below. const reqCtx: RequestContext = requestContextFromRequest(webRequest); // ── 4. Run middleware ───────────────────────────────────────── @@ -846,6 +849,11 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { } } + // Rebuild context after middleware has unpacked x-middleware-request-* + // headers into webRequest. Used only for afterFiles and fallback rewrites, + // which run after middleware in the App Router execution order. + const postMwReqCtx: RequestContext = requestContextFromRequest(webRequest); + let resolvedPathname = resolvedUrl.split("?")[0]; // ── 5. Apply custom headers from next.config.js ─────────────── @@ -893,7 +901,7 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { // ── 7. Apply beforeFiles rewrites from next.config.js ───────── if (configRewrites.beforeFiles?.length) { - const rewritten = matchRewrite(resolvedPathname, configRewrites.beforeFiles, reqCtx); + const rewritten = matchRewrite(resolvedPathname, configRewrites.beforeFiles, postMwReqCtx); if (rewritten) { if (isExternalUrl(rewritten)) { const proxyResponse = await proxyExternalRequest(webRequest, rewritten); @@ -929,7 +937,7 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { // ── 9. Apply afterFiles rewrites from next.config.js ────────── if (configRewrites.afterFiles?.length) { - const rewritten = matchRewrite(resolvedPathname, configRewrites.afterFiles, reqCtx); + const rewritten = matchRewrite(resolvedPathname, configRewrites.afterFiles, postMwReqCtx); if (rewritten) { if (isExternalUrl(rewritten)) { const proxyResponse = await proxyExternalRequest(webRequest, rewritten); @@ -948,7 +956,7 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { // ── 11. Fallback rewrites (if SSR returned 404) ───────────── if (response && response.status === 404 && configRewrites.fallback?.length) { - const fallbackRewrite = matchRewrite(resolvedPathname, configRewrites.fallback, reqCtx); + const fallbackRewrite = matchRewrite(resolvedPathname, configRewrites.fallback, postMwReqCtx); if (fallbackRewrite) { if (isExternalUrl(fallbackRewrite)) { const proxyResponse = await proxyExternalRequest(webRequest, fallbackRewrite); diff --git a/packages/vinext/src/shims/headers.ts b/packages/vinext/src/shims/headers.ts index f707bd26..593cbeba 100644 --- a/packages/vinext/src/shims/headers.ts +++ b/packages/vinext/src/shims/headers.ts @@ -132,6 +132,15 @@ export function consumeDynamicUsage(): boolean { * in-place and is only safe for cleanup (ctx=null) within an existing * als.run() scope. */ +/** + * Returns the current live HeadersContext from ALS (or the fallback). + * Used after applyMiddlewareRequestHeaders() to build a post-middleware + * request context for afterFiles/fallback rewrite has/missing evaluation. + */ +export function getHeadersContext(): HeadersContext | null { + return _getState().headersContext; +} + export function setHeadersContext(ctx: HeadersContext | null): void { if (ctx !== null) { // For backward compatibility, set context on the current ALS store diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index c120fd4c..bc9513ca 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -1960,6 +1960,40 @@ describe("App Router next.config.js features (dev server integration)", () => { expect(html).toContain("About"); }); + // In App Router execution order, beforeFiles rewrites run after middleware. + // has/missing conditions on beforeFiles rules should therefore evaluate against + // middleware-modified headers/cookies, not the original pre-middleware request. + it("beforeFiles rewrite has/missing conditions see middleware-injected cookies", async () => { + // Without ?mw-auth, middleware does NOT inject mw-before-user=1. + // The has:[cookie:mw-before-user] beforeFiles rule should NOT match → no rewrite. + const noAuthRes = await fetch(`${baseUrl}/mw-gated-before`); + expect(noAuthRes.status).toBe(404); + + // With ?mw-auth, middleware injects mw-before-user=1 into request cookies. + // The has:[cookie:mw-before-user] beforeFiles rule SHOULD match → rewrite to /about. + const authRes = await fetch(`${baseUrl}/mw-gated-before?mw-auth`); + expect(authRes.status).toBe(200); + const html = await authRes.text(); + expect(html).toContain("About"); + }); + + // Fallback rewrites run after middleware and after a 404 from route matching. + // has/missing conditions on fallback rules should evaluate against + // middleware-modified headers/cookies, not the original pre-middleware request. + it("fallback rewrite has/missing conditions see middleware-injected cookies", async () => { + // Without ?mw-auth, middleware does NOT inject mw-fallback-user=1. + // The has:[cookie:mw-fallback-user] fallback rule should NOT match → 404. + const noAuthRes = await fetch(`${baseUrl}/mw-gated-fallback`); + expect(noAuthRes.status).toBe(404); + + // With ?mw-auth, middleware injects mw-fallback-user=1 into request cookies. + // The has:[cookie:mw-fallback-user] fallback rule SHOULD match → rewrite to /about. + const authRes = await fetch(`${baseUrl}/mw-gated-fallback?mw-auth`); + expect(authRes.status).toBe(200); + const html = await authRes.text(); + expect(html).toContain("About"); + }); + it("applies custom headers from next.config.js on API routes", async () => { const res = await fetch(`${baseUrl}/api/hello`); expect(res.headers.get("x-custom-header")).toBe("vinext-app"); @@ -2163,8 +2197,9 @@ describe("App Router next.config.js features (generateRscEntry)", () => { it("strips .rsc suffix before matching beforeFiles rewrite rules", () => { // RSC (soft-nav) requests arrive as /some/path.rsc but rewrite patterns // are defined without the extension. The generated code must strip .rsc - // before calling __applyConfigRewrites for beforeFiles, just like it does - // for redirects. + // before calling __applyConfigRewrites for beforeFiles. + // beforeFiles now runs after middleware (using __postMwReqCtx), and + // cleanPathname has already had .rsc stripped at that point. const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, { rewrites: { beforeFiles: [{ source: "/old", destination: "/new" }], @@ -2172,14 +2207,14 @@ describe("App Router next.config.js features (generateRscEntry)", () => { fallback: [], }, }); - // The generated code should use a .rsc-stripped pathname variable when - // calling __applyConfigRewrites for beforeFiles, not the raw `pathname`. - const rewritePathIdx = code.indexOf("__rewritePathname"); - expect(rewritePathIdx).toBeGreaterThan(-1); - // The .rsc stripping assignment must appear before the beforeFiles rewrite call - const beforeFilesCallIdx = code.indexOf("__applyConfigRewrites(__rewritePathname"); + // The generated code uses cleanPathname (already .rsc-stripped) when + // calling __applyConfigRewrites for beforeFiles. + const beforeFilesCallIdx = code.indexOf("__applyConfigRewrites(cleanPathname, __configRewrites.beforeFiles"); expect(beforeFilesCallIdx).toBeGreaterThan(-1); - expect(rewritePathIdx).toBeLessThan(beforeFilesCallIdx); + // The cleanPathname assignment (stripping .rsc) must appear before the beforeFiles call + const cleanPathnameIdx = code.indexOf("cleanPathname = pathname.replace"); + expect(cleanPathnameIdx).toBeGreaterThan(-1); + expect(cleanPathnameIdx).toBeLessThan(beforeFilesCallIdx); }); it("applies afterFiles rewrites in the handler code", () => { diff --git a/tests/fixtures/app-basic/middleware.ts b/tests/fixtures/app-basic/middleware.ts index 4022d5be..d55c11d2 100644 --- a/tests/fixtures/app-basic/middleware.ts +++ b/tests/fixtures/app-basic/middleware.ts @@ -60,6 +60,27 @@ export function middleware(request: NextRequest) { throw new Error("middleware crash"); } + // Inject mw-before-user=1 cookie for beforeFiles rewrite gating test. + // In App Router order, beforeFiles rewrites run after middleware, so they + // should see this cookie. The /mw-gated-before rule in next.config.ts has: + // [cookie:mw-before-user], which matches when ?mw-auth is present. + if (pathname === "/mw-gated-before" && request.nextUrl.searchParams.has("mw-auth")) { + const headers = new Headers(request.headers); + const existing = headers.get("cookie") ?? ""; + headers.set("cookie", existing ? existing + "; mw-before-user=1" : "mw-before-user=1"); + return NextResponse.next({ request: { headers } }); + } + + // Inject mw-fallback-user=1 cookie for fallback rewrite gating test. + // Fallback rewrites run after middleware and after a 404 from route matching. + // The /mw-gated-fallback rule has: [cookie:mw-fallback-user]. + if (pathname === "/mw-gated-fallback" && request.nextUrl.searchParams.has("mw-auth")) { + const headers = new Headers(request.headers); + const existing = headers.get("cookie") ?? ""; + headers.set("cookie", existing ? existing + "; mw-fallback-user=1" : "mw-fallback-user=1"); + return NextResponse.next({ request: { headers } }); + } + // Forward search params as a header for RSC testing // Ref: opennextjs-cloudflare middleware.ts — search-params header const requestHeaders = new Headers(request.headers); @@ -88,5 +109,7 @@ export const config = { "/middleware-throw", "/search-query", "/", + "/mw-gated-before", + "/mw-gated-fallback", ], }; diff --git a/tests/fixtures/app-basic/next.config.ts b/tests/fixtures/app-basic/next.config.ts index d52ca925..cd881bb1 100644 --- a/tests/fixtures/app-basic/next.config.ts +++ b/tests/fixtures/app-basic/next.config.ts @@ -73,6 +73,14 @@ const nextConfig: NextConfig = { ...(process.env.TEST_EXTERNAL_PROXY_TARGET ? [{ source: "/proxy-external-test/:path*", destination: `${process.env.TEST_EXTERNAL_PROXY_TARGET}/:path*` }] : []), + // Used by Vitest: app-router.test.ts — beforeFiles rewrite gated on a + // cookie injected by middleware. In App Router order, beforeFiles runs + // after middleware, so it should see middleware-injected cookies. + { + source: "/mw-gated-before", + has: [{ type: "cookie", key: "mw-before-user" }], + destination: "/about", + }, ], afterFiles: [ // Used by Vitest: app-router.test.ts @@ -80,7 +88,17 @@ const nextConfig: NextConfig = { // Used by E2E: config-redirect.spec.ts { source: "/config-rewrite", destination: "/" }, ], - fallback: [], + fallback: [ + // Used by Vitest: app-router.test.ts — fallback rewrite gated on a + // cookie injected by middleware. Fallback runs after middleware and + // after a 404 from route matching, so it should see middleware-injected + // cookies. /mw-gated-fallback has no real page, so it falls through. + { + source: "/mw-gated-fallback", + has: [{ type: "cookie", key: "mw-fallback-user" }], + destination: "/about", + }, + ], }; }, diff --git a/tests/fixtures/pages-basic/middleware.ts b/tests/fixtures/pages-basic/middleware.ts index d08b3194..a7cea7e7 100644 --- a/tests/fixtures/pages-basic/middleware.ts +++ b/tests/fixtures/pages-basic/middleware.ts @@ -46,6 +46,27 @@ export function middleware(request: NextRequest) { return NextResponse.next({ request: { headers } }); } + // Inject mw-user=1 cookie for afterFiles rewrite gating test. + // afterFiles rewrites run after middleware, so they should see this cookie. + // The /mw-gated-rewrite rule in next.config.mjs has: [cookie:mw-user], + // which should match when ?mw-auth is present and middleware injects it. + if (url.pathname === "/mw-gated-rewrite" && url.searchParams.has("mw-auth")) { + const headers = new Headers(request.headers); + const existing = headers.get("cookie") ?? ""; + headers.set("cookie", existing ? existing + "; mw-user=1" : "mw-user=1"); + return NextResponse.next({ request: { headers } }); + } + + // Inject mw-before-user=1 cookie for beforeFiles rewrite gating test. + // beforeFiles rewrites run after middleware per Next.js docs, so they + // should see this cookie. The /mw-gated-before rule has: [cookie:mw-before-user]. + if (url.pathname === "/mw-gated-before" && url.searchParams.has("mw-auth")) { + const headers = new Headers(request.headers); + const existing = headers.get("cookie") ?? ""; + headers.set("cookie", existing ? existing + "; mw-before-user=1" : "mw-before-user=1"); + return NextResponse.next({ request: { headers } }); + } + return response; } diff --git a/tests/fixtures/pages-basic/next.config.mjs b/tests/fixtures/pages-basic/next.config.mjs index 1fff4d6e..2cbd727a 100644 --- a/tests/fixtures/pages-basic/next.config.mjs +++ b/tests/fixtures/pages-basic/next.config.mjs @@ -19,12 +19,30 @@ const nextConfig = { source: "/before-rewrite", destination: "/about", }, + // Used by Vitest: pages-router.test.ts — beforeFiles rewrite gated on + // a cookie injected by middleware. Middleware injects mw-before-user=1 + // when ?mw-auth is present. The beforeFiles rewrite should see this + // cookie because beforeFiles runs after middleware per Next.js docs. + { + source: "/mw-gated-before", + has: [{ type: "cookie", key: "mw-before-user" }], + destination: "/about", + }, ], afterFiles: [ { source: "/after-rewrite", destination: "/about", }, + // Used by Vitest: pages-router.test.ts — afterFiles rewrite gated on + // a cookie injected by middleware. Middleware injects mw-user=1 via + // x-middleware-request-cookie when ?mw-auth is present. The afterFiles + // rewrite should see this cookie because afterFiles runs after middleware. + { + source: "/mw-gated-rewrite", + has: [{ type: "cookie", key: "mw-user" }], + destination: "/about", + }, ], fallback: [ { diff --git a/tests/pages-router.test.ts b/tests/pages-router.test.ts index b9c9be69..8223b891 100644 --- a/tests/pages-router.test.ts +++ b/tests/pages-router.test.ts @@ -1514,6 +1514,41 @@ describe("Production server next.config.js features (Pages Router)", () => { expect(vary).toContain("Accept-Language"); }); + // afterFiles rewrites run after middleware in the App Router execution order. + // has/missing conditions on afterFiles rules should evaluate against + // middleware-modified headers, not the original pre-middleware request. + it("afterFiles rewrite has/missing conditions see middleware-injected cookies", async () => { + // Without ?mw-auth, middleware does NOT inject mw-user=1. + // The has:[cookie:mw-user] afterFiles rule should NOT match → no rewrite. + const noAuthRes = await fetch(`${prodUrl}/mw-gated-rewrite`); + expect(noAuthRes.status).toBe(404); + + // With ?mw-auth, middleware injects mw-user=1 into request cookies. + // The has:[cookie:mw-user] afterFiles rule SHOULD match → rewrite to /about. + const authRes = await fetch(`${prodUrl}/mw-gated-rewrite?mw-auth`); + expect(authRes.status).toBe(200); + const html = await authRes.text(); + expect(html).toContain("About"); + }); + + // beforeFiles rewrites run after middleware per the Next.js execution order: + // headers → redirects → Middleware → beforeFiles → filesystem → afterFiles → fallback. + // has/missing conditions on beforeFiles rules should evaluate against + // middleware-modified headers, not the original pre-middleware request. + it("beforeFiles rewrite has/missing conditions see middleware-injected cookies", async () => { + // Without ?mw-auth, middleware does NOT inject mw-before-user=1. + // The has:[cookie:mw-before-user] beforeFiles rule should NOT match → 404. + const noAuthRes = await fetch(`${prodUrl}/mw-gated-before`); + expect(noAuthRes.status).toBe(404); + + // With ?mw-auth, middleware injects mw-before-user=1 into request cookies. + // The has:[cookie:mw-before-user] beforeFiles rule SHOULD match → rewrite to /about. + const authRes = await fetch(`${prodUrl}/mw-gated-before?mw-auth`); + expect(authRes.status).toBe(200); + const html = await authRes.text(); + expect(html).toContain("About"); + }); + it("serves normal pages unaffected by config rules", async () => { const res = await fetch(`${prodUrl}/`); expect(res.status).toBe(200); @@ -1639,7 +1674,7 @@ describe("Static export (Pages Router)", () => { "utf-8", ); expect(html404).toContain("404"); - }); + }); it("escapes meta refresh URL to prevent HTML injection", async () => { expect(fs.existsSync(path.join(exportDir, "redirect-xss.html"))).toBe(true);