diff --git a/packages/vinext/src/build/prerender.ts b/packages/vinext/src/build/prerender.ts index 10ed20a20..274d802af 100644 --- a/packages/vinext/src/build/prerender.ts +++ b/packages/vinext/src/build/prerender.ts @@ -897,12 +897,21 @@ export async function prerenderApp({ rscHandler = (req: Request) => { // Forward the request to the local prod server. + // `redirect: "manual"` ensures pages that call `redirect()` surface as + // their original 3xx response — otherwise fetch follows the Location + // header server-side, the prerender harness sees a 200 for the + // destination page, and that destination HTML gets written under the + // redirecting route's filename. At runtime the prod server then serves + // the cached HTML with status 200 instead of emitting a 307 for the + // document load. Mirrors the pages-prerender `renderPage` helper above. + // See: https://github.com/cloudflare/vinext/issues/1530 const parsed = new URL(req.url); const url = `${baseUrl}${parsed.pathname}${parsed.search}`; return fetch(url, { method: req.method, headers: { ...secretHeaders, ...Object.fromEntries(req.headers.entries()) }, body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined, + redirect: "manual", }); }; diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index eb4bb4c5c..cf709a980 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -2440,6 +2440,23 @@ describe("App Router Production server (startProdServer)", () => { expect(res.status).toBe(404); }); + // Ported from Next.js: test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts + // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts + // + // Document request (no `Rsc` header) to a page that calls `redirect()` must + // respond with HTTP 307 + Location. The RSC variant (`.rsc` URL or Rsc:1 + // header) returns 200 with a flight payload — that path is covered by the + // sibling `.rsc` redirect tests above and by issue #1347. + // + // See: https://github.com/cloudflare/vinext/issues/1530 + it("redirect() from Server Component returns 307 on document load (production)", async () => { + const res = await fetch(`${baseUrl}/redirect-test`, { redirect: "manual" }); + expect(res.status).toBe(307); + const location = res.headers.get("location"); + expect(location).toBeTruthy(); + expect(location).toContain("/about"); + }); + it("serves static assets with cache headers", async () => { // Find an actual hashed asset from the build (on disk under // `_next/static/`, matching `resolveAssetsDir("")`). diff --git a/tests/prerender.test.ts b/tests/prerender.test.ts index 4124b17d9..d9a5c9f78 100644 --- a/tests/prerender.test.ts +++ b/tests/prerender.test.ts @@ -927,6 +927,27 @@ describe("prerenderApp — default mode (app-basic)", () => { expect(r?.status).toBe("skipped"); }); + // Ported from Next.js: test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts + // ('should get 307 status code for document request') + // + // A speculative prerender of a route that calls `redirect()` must not + // follow the redirect server-side and cache the destination's HTML under + // the redirecting URL. Doing so makes the prod server reply with 200 and + // the destination's body on every document request to the redirecting + // route, instead of emitting an HTTP 307 with a Location header. + // + // See: https://github.com/cloudflare/vinext/issues/1530 + it("skips /redirect-test instead of capturing the destination HTML", () => { + const r = findRoute(results, "/redirect-test"); + expect(r).toBeDefined(); + expect(r?.status).toBe("skipped"); + // No HTML/RSC must be written for the redirecting route — otherwise the + // prod server serves the cached destination body with status 200 for + // every document request to /redirect-test. + expect(fs.existsSync(path.join(outDir, "redirect-test.html"))).toBe(false); + expect(fs.existsSync(path.join(outDir, "redirect-test.rsc"))).toBe(false); + }); + // ── API routes — always skipped ──────────────────────────────────────────── it("skips all API route handlers", () => {