From 0c17c5eaef8c0382e28124b59f209cefbb3bde6a Mon Sep 17 00:00:00 2001 From: magnus Date: Sat, 7 Feb 2026 00:18:36 +0100 Subject: [PATCH 1/8] fix(pages-router): Add patch for trustHostHeader using res.revalidate --- .../pages-router/src/pages/api/revalidate.ts | 26 +++ .../src/pages/revalidate/[key].tsx | 69 +++++++ packages/open-next/src/adapters/cache.ts | 2 + .../open-next/src/build/createServerBundle.ts | 1 + .../src/build/patch/patches/index.ts | 1 + .../patch/patches/patchPagesApiRuntimeProd.ts | 91 +++++++++ .../tests/pagesRouter/revalidate.test.ts | 25 +++ .../patches/patchPagesApiRuntimeProd.test.ts | 187 ++++++++++++++++++ 8 files changed, 402 insertions(+) create mode 100644 examples/pages-router/src/pages/api/revalidate.ts create mode 100644 examples/pages-router/src/pages/revalidate/[key].tsx create mode 100644 packages/open-next/src/build/patch/patches/patchPagesApiRuntimeProd.ts create mode 100644 packages/tests-e2e/tests/pagesRouter/revalidate.test.ts create mode 100644 packages/tests-unit/tests/build/patch/patches/patchPagesApiRuntimeProd.test.ts diff --git a/examples/pages-router/src/pages/api/revalidate.ts b/examples/pages-router/src/pages/api/revalidate.ts new file mode 100644 index 000000000..c979e6c65 --- /dev/null +++ b/examples/pages-router/src/pages/api/revalidate.ts @@ -0,0 +1,26 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +type Data = + | { revalidated: true; path: string } + | { revalidated: false; message: string }; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== "POST" && req.method !== "GET") { + return res + .status(405) + .json({ revalidated: false, message: "Method not allowed" }); + } + + const key = req.query.key; + if (!key || typeof key !== "string") { + return res.status(400).json({ revalidated: false, message: "Missing key" }); + } + + const path = `/revalidate/${key}`; + + await res.revalidate(path); + return res.status(200).json({ revalidated: true, path }); +} diff --git a/examples/pages-router/src/pages/revalidate/[key].tsx b/examples/pages-router/src/pages/revalidate/[key].tsx new file mode 100644 index 000000000..7c6be152f --- /dev/null +++ b/examples/pages-router/src/pages/revalidate/[key].tsx @@ -0,0 +1,69 @@ +import type { GetStaticPropsContext, GetStaticPropsResult } from "next"; +import Head from "next/head"; + +type Params = { key: string }; + +type FakeRecord = { + key: string; + title: string; + updatedAt: string; +}; + +type Props = { + record: FakeRecord; +}; + +const fakeDb: FakeRecord[] = [ + { key: "1", title: "First record", updatedAt: new Date().toISOString() }, + { key: "2", title: "Second record", updatedAt: new Date().toISOString() }, + { key: "3", title: "Third record", updatedAt: new Date().toISOString() }, +]; + +export default function TestKeyPage({ record }: Props) { + return ( +
+ + SSG Test — {record.key} + +

SSG Test Page

+

+ Key: {record.key} +

+

+ Title: {record.title} +

+

+ Updated:{" "} + {record.updatedAt} +

+

Revalidate set in getStaticProps.

+
+ ); +} + +export async function getStaticProps({ + params, +}: GetStaticPropsContext): Promise> { + const found = fakeDb.find((item) => item.key === params?.key); + + if (!found) { + return { notFound: true }; + } + + return { + props: { + record: { + ...found, + updatedAt: new Date().toISOString(), + }, + }, + }; +} + +export async function getStaticPaths() { + const paths = fakeDb.map((item) => ({ + params: { key: item.key }, + })); + + return { paths, fallback: "blocking" }; +} diff --git a/packages/open-next/src/adapters/cache.ts b/packages/open-next/src/adapters/cache.ts index 89f42c480..61b78f400 100644 --- a/packages/open-next/src/adapters/cache.ts +++ b/packages/open-next/src/adapters/cache.ts @@ -207,6 +207,7 @@ export default class Cache { if (globalThis.openNextConfig.dangerous?.disableIncrementalCache) { return; } + console.log("SET CACHE", { key, data, ctx }); // This one might not even be necessary anymore // Better be safe than sorry const detachedPromise = globalThis.__openNextAls @@ -314,6 +315,7 @@ export default class Cache { await this.updateTagsOnSet(key, data, ctx); debug("Finished setting cache"); } catch (e) { + console.log("ARE WE HERE"); error("Failed to set cache", e); } finally { // We need to resolve the promise even if there was an error diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 6cb0d02c6..53adffcca 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -207,6 +207,7 @@ async function generateBundle( patches.patchBackgroundRevalidation, patches.patchUseCacheForISR, patches.patchNodeEnvironment, + patches.patchPagesApiRuntimeProd, ...additionalCodePatches, ]); diff --git a/packages/open-next/src/build/patch/patches/index.ts b/packages/open-next/src/build/patch/patches/index.ts index 055bb5de2..578f1f45c 100644 --- a/packages/open-next/src/build/patch/patches/index.ts +++ b/packages/open-next/src/build/patch/patches/index.ts @@ -9,3 +9,4 @@ export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.j export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js"; export { patchNodeEnvironment } from "./patchNodeEnvironment.js"; export { patchOriginalNextConfig } from "./patchOriginalNextConfig.js"; +export { patchPagesApiRuntimeProd } from "./patchPagesApiRuntimeProd.js"; diff --git a/packages/open-next/src/build/patch/patches/patchPagesApiRuntimeProd.ts b/packages/open-next/src/build/patch/patches/patchPagesApiRuntimeProd.ts new file mode 100644 index 000000000..02eff3a6e --- /dev/null +++ b/packages/open-next/src/build/patch/patches/patchPagesApiRuntimeProd.ts @@ -0,0 +1,91 @@ +import { getCrossPlatformPathRegex } from "utils/regex.js"; +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; + +// `context.trustHostHeader` is undefined in our case +// Trust the host header when invoking `res.revalidate("/path")` from pages router +// https://github.com/vercel/next.js/blob/178a4c7/packages/next/src/server/api-utils/node/api-resolver.ts#L301 +export const trustHostHeaderRule = ` +rule: + kind: member_expression + pattern: $CONTEXT.trustHostHeader + inside: + kind: parenthesized_expression + inside: + kind: if_statement + all: + - has: + regex: await + kind: statement_block + has: + kind: lexical_declaration + regex: HEAD + has: + kind: variable_declarator + has: + kind: await_expression + has: + kind: call_expression + has: + kind: identifier + regex: ^fetch$ +fix: + 'true' +`; + +// Use correct protocol from `NextInternalRequestMeta` when doing HEAD fetch for revalidation +export const headFetchProtocolRule = ` +rule: + kind: string_fragment + regex: ^https:// + inside: + kind: template_string + inside: + kind: arguments + has: + kind: object + regex: HEAD + inside: + kind: call_expression + inside: + kind: await_expression + regex: fetch + inside: + kind: variable_declarator + inside: + kind: lexical_declaration + regex: x-vercel-cache + inside: + kind: statement_block + inside: + kind: if_statement +fix: + '\${r.headers["x-forwarded-proto"] || "https"}://' +`; + +const pathFilter = getCrossPlatformPathRegex( + String.raw`/next/dist/compiled/next-server/pages-api(-turbo)?\.runtime\.prod\.js$`, + { + escape: false, + }, +); + +export const patchPagesApiRuntimeProd: CodePatcher = { + name: "patch-pages-api-runtime-prod", + patches: [ + // Trust the host header when invoking `res.revalidate("") from pages router + { + pathFilter, + contentFilter: /trustHostHeader/, + patchCode: createPatchCode(trustHostHeaderRule), + versions: ">=15.0.0", + }, + // Use correct protocol from `NextInternalRequestMeta` when doing HEAD fetch for revalidation + { + pathFilter, + contentFilter: /https/, + patchCode: createPatchCode(headFetchProtocolRule), + versions: ">=15.0.0", + }, + ], +}; diff --git a/packages/tests-e2e/tests/pagesRouter/revalidate.test.ts b/packages/tests-e2e/tests/pagesRouter/revalidate.test.ts new file mode 100644 index 000000000..d7f9e9fa1 --- /dev/null +++ b/packages/tests-e2e/tests/pagesRouter/revalidate.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "@playwright/test"; + +test("res.revalidate should work", async ({ page }) => { + await page.goto("/revalidate/1"); + + const initialUpdatedAt = await page.getByTestId("updated-at").textContent(); + + const res = await page.request.post("/api/revalidate?key=1"); + const json = await res.json(); + + expect(json).toEqual({ revalidated: true, path: "/revalidate/1" }); + + if (!res.ok()) { + throw new Error(`Failed to trigger revalidation: ${await res.text()}`); + } + + // Wait for a short period to allow revalidation to complete + await page.waitForTimeout(1000); + await page.goto("/revalidate/1"); + + const updatedUpdatedAt = await page.getByTestId("updated-at").textContent(); + expect(new Date(updatedUpdatedAt!).getTime()).toBeGreaterThan( + new Date(initialUpdatedAt!).getTime(), + ); +}); diff --git a/packages/tests-unit/tests/build/patch/patches/patchPagesApiRuntimeProd.test.ts b/packages/tests-unit/tests/build/patch/patches/patchPagesApiRuntimeProd.test.ts new file mode 100644 index 000000000..87dfbdaee --- /dev/null +++ b/packages/tests-unit/tests/build/patch/patches/patchPagesApiRuntimeProd.test.ts @@ -0,0 +1,187 @@ +import { + headFetchProtocolRule, + trustHostHeaderRule, +} from "@opennextjs/aws/build/patch/patches/patchPagesApiRuntimeProd"; +import { describe, expect, it } from "vitest"; +import { computePatchDiff } from "./util.js"; + +const pagesResRevalidateCodeBundled = ` +class NextNodeServer extends _baseserver.default { +async function tO(e,t,r,n){if("string"!=typeof e||!e.startsWith("/"))throw Object.defineProperty(Error(\`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received \${e}\`),"__NEXT_ERROR_CODE",{value:"E153",enumerable:!1,configurable:!0});let i={[x.y3]:n.previewModeId,...t.unstable_onlyGenerated?{[x.Qq]:"1"}:{}},a=[...n.allowedRevalidateHeaderKeys||[]];for(let e of((n.trustHostHeader||n.dev)&&a.push("cookie"),n.trustHostHeader&&a.push("x-vercel-protection-bypass"),Object.keys(r.headers)))a.includes(e)&&(i[e]=r.headers[e]);let s=n.internalRevalidate;try{if(s)return await s({urlPath:e,revalidateHeaders:i,opts:t});if(r.trustHostHeader){let n=await fetch(\`https://\${r.headers.host}\${e}\`,{method:"HEAD",headers:i}),a=n.headers.get("x-vercel-cache")||n.headers.get("x-nextjs-cache");if((null==a?void 0:a.toUpperCase())!=="REVALIDATED"&&200!==n.status&&!(404===n.status&&t.unstable_onlyGenerated))throw Object.defineProperty(Error(\`Invalid response \${n.status}\`),"__NEXT_ERROR_CODE",{value:"E175",enumerable:!1,configurable:!0})}else throw Object.defineProperty(Error("Invariant: missing internal router-server-methods this is an internal bug"),"__NEXT_ERROR_CODE",{value:"E676",enumerable:!1,configurable:!0})}catch(t){throw Object.defineProperty(Error(\`Failed to revalidate \${e}: \${t_(t)?t.message:t}\`),"__NEXT_ERROR_CODE",{value:"E240",enumerable:!1,configurable:!0})}} +`; + +const pagesResRevalidateCodeUnbundled = ` +async function revalidate(urlPath, opts, req, context) { + if (typeof urlPath !== 'string' || !urlPath.startsWith('/')) { + throw Object.defineProperty(new Error(\`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received \${urlPath}\`), "__NEXT_ERROR_CODE", { + value: "E153", + enumerable: false, + configurable: true + }); + } + const revalidateHeaders = { + [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, + ...opts.unstable_onlyGenerated ? { + [PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER]: '1' + } : {} + }; + const allowedRevalidateHeaderKeys = [ + ...context.allowedRevalidateHeaderKeys || [] + ]; + if (context.trustHostHeader || context.dev) { + allowedRevalidateHeaderKeys.push('cookie'); + } + if (context.trustHostHeader) { + allowedRevalidateHeaderKeys.push('x-vercel-protection-bypass'); + } + for (const key of Object.keys(req.headers)){ + if (allowedRevalidateHeaderKeys.includes(key)) { + revalidateHeaders[key] = req.headers[key]; + } + } + const internalRevalidate = context.internalRevalidate; + try { + // We use the revalidate in router-server if available. + // If we are operating without router-server (serverless) + // we must go through network layer with fetch request + if (internalRevalidate) { + return await internalRevalidate({ + urlPath, + revalidateHeaders, + opts + }); + } + if (context.trustHostHeader) { + const res = await fetch(\`https://\${req.headers.host}\${urlPath}\`, { + method: 'HEAD', + headers: revalidateHeaders + }); + // we use the cache header to determine successful revalidate as + // a non-200 status code can be returned from a successful revalidate + // e.g. notFound: true returns 404 status code but is successful + const cacheHeader = res.headers.get('x-vercel-cache') || res.headers.get('x-nextjs-cache'); + if ((cacheHeader == null ? void 0 : cacheHeader.toUpperCase()) !== 'REVALIDATED' && res.status !== 200 && !(res.status === 404 && opts.unstable_onlyGenerated)) { + throw Object.defineProperty(new Error(\`Invalid response \${res.status}\`), "__NEXT_ERROR_CODE", { + value: "E175", + enumerable: false, + configurable: true + }); + } + } else { + throw Object.defineProperty(new Error(\`Invariant: missing internal router-server-methods this is an internal bug\`), "__NEXT_ERROR_CODE", { + value: "E676", + enumerable: false, + configurable: true + }); + } + } catch (err) { + throw Object.defineProperty(new Error(\`Failed to revalidate \${urlPath}: \${isError(err) ? err.message : err}\`), "__NEXT_ERROR_CODE", { + value: "E240", + enumerable: false, + configurable: true + }); + } +}`; + +describe("patchPagesApiRuntimeProd", () => { + describe("bundled res.revalidate code", () => { + it("should patch trustHostHeader", async () => { + expect( + computePatchDiff( + "pages-api.runtime.prod.js", + pagesResRevalidateCodeBundled, + trustHostHeaderRule, + ), + ).toMatchInlineSnapshot(` + "Index: pages-api.runtime.prod.js + =================================================================== + --- pages-api.runtime.prod.js + +++ pages-api.runtime.prod.js + @@ -1,3 +1,2 @@ + - + class NextNodeServer extends _baseserver.default { + -async function tO(e,t,r,n){if("string"!=typeof e||!e.startsWith("/"))throw Object.defineProperty(Error(\`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received \${e}\`),"__NEXT_ERROR_CODE",{value:"E153",enumerable:!1,configurable:!0});let i={[x.y3]:n.previewModeId,...t.unstable_onlyGenerated?{[x.Qq]:"1"}:{}},a=[...n.allowedRevalidateHeaderKeys||[]];for(let e of((n.trustHostHeader||n.dev)&&a.push("cookie"),n.trustHostHeader&&a.push("x-vercel-protection-bypass"),Object.keys(r.headers)))a.includes(e)&&(i[e]=r.headers[e]);let s=n.internalRevalidate;try{if(s)return await s({urlPath:e,revalidateHeaders:i,opts:t});if(r.trustHostHeader){let n=await fetch(\`https://\${r.headers.host}\${e}\`,{method:"HEAD",headers:i}),a=n.headers.get("x-vercel-cache")||n.headers.get("x-nextjs-cache");if((null==a?void 0:a.toUpperCase())!=="REVALIDATED"&&200!==n.status&&!(404===n.status&&t.unstable_onlyGenerated))throw Object.defineProperty(Error(\`Invalid response \${n.status}\`),"__NEXT_ERROR_CODE",{value:"E175",enumerable:!1,configurable:!0})}else throw Object.defineProperty(Error("Invariant: missing internal router-server-methods this is an internal bug"),"__NEXT_ERROR_CODE",{value:"E676",enumerable:!1,configurable:!0})}catch(t){throw Object.defineProperty(Error(\`Failed to revalidate \${e}: \${t_(t)?t.message:t}\`),"__NEXT_ERROR_CODE",{value:"E240",enumerable:!1,configurable:!0})}} + +async function tO(e,t,r,n){if("string"!=typeof e||!e.startsWith("/"))throw Object.defineProperty(Error(\`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received \${e}\`),"__NEXT_ERROR_CODE",{value:"E153",enumerable:!1,configurable:!0});let i={[x.y3]:n.previewModeId,...t.unstable_onlyGenerated?{[x.Qq]:"1"}:{}},a=[...n.allowedRevalidateHeaderKeys||[]];for(let e of((n.trustHostHeader||n.dev)&&a.push("cookie"),n.trustHostHeader&&a.push("x-vercel-protection-bypass"),Object.keys(r.headers)))a.includes(e)&&(i[e]=r.headers[e]);let s=n.internalRevalidate;try{if(s)return await s({urlPath:e,revalidateHeaders:i,opts:t});if(true){let n=await fetch(\`https://\${r.headers.host}\${e}\`,{method:"HEAD",headers:i}),a=n.headers.get("x-vercel-cache")||n.headers.get("x-nextjs-cache");if((null==a?void 0:a.toUpperCase())!=="REVALIDATED"&&200!==n.status&&!(404===n.status&&t.unstable_onlyGenerated))throw Object.defineProperty(Error(\`Invalid response \${n.status}\`),"__NEXT_ERROR_CODE",{value:"E175",enumerable:!1,configurable:!0})}else throw Object.defineProperty(Error("Invariant: missing internal router-server-methods this is an internal bug"),"__NEXT_ERROR_CODE",{value:"E676",enumerable:!1,configurable:!0})}catch(t){throw Object.defineProperty(Error(\`Failed to revalidate \${e}: \${t_(t)?t.message:t}\`),"__NEXT_ERROR_CODE",{value:"E240",enumerable:!1,configurable:!0})}} + " + `); + }); + + it("should set correct protocol", async () => { + expect( + computePatchDiff( + "pages-api.runtime.prod.js", + pagesResRevalidateCodeBundled, + headFetchProtocolRule, + ), + ).toMatchInlineSnapshot(` + "Index: pages-api.runtime.prod.js + =================================================================== + --- pages-api.runtime.prod.js + +++ pages-api.runtime.prod.js + @@ -1,3 +1,2 @@ + - + class NextNodeServer extends _baseserver.default { + -async function tO(e,t,r,n){if("string"!=typeof e||!e.startsWith("/"))throw Object.defineProperty(Error(\`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received \${e}\`),"__NEXT_ERROR_CODE",{value:"E153",enumerable:!1,configurable:!0});let i={[x.y3]:n.previewModeId,...t.unstable_onlyGenerated?{[x.Qq]:"1"}:{}},a=[...n.allowedRevalidateHeaderKeys||[]];for(let e of((n.trustHostHeader||n.dev)&&a.push("cookie"),n.trustHostHeader&&a.push("x-vercel-protection-bypass"),Object.keys(r.headers)))a.includes(e)&&(i[e]=r.headers[e]);let s=n.internalRevalidate;try{if(s)return await s({urlPath:e,revalidateHeaders:i,opts:t});if(r.trustHostHeader){let n=await fetch(\`https://\${r.headers.host}\${e}\`,{method:"HEAD",headers:i}),a=n.headers.get("x-vercel-cache")||n.headers.get("x-nextjs-cache");if((null==a?void 0:a.toUpperCase())!=="REVALIDATED"&&200!==n.status&&!(404===n.status&&t.unstable_onlyGenerated))throw Object.defineProperty(Error(\`Invalid response \${n.status}\`),"__NEXT_ERROR_CODE",{value:"E175",enumerable:!1,configurable:!0})}else throw Object.defineProperty(Error("Invariant: missing internal router-server-methods this is an internal bug"),"__NEXT_ERROR_CODE",{value:"E676",enumerable:!1,configurable:!0})}catch(t){throw Object.defineProperty(Error(\`Failed to revalidate \${e}: \${t_(t)?t.message:t}\`),"__NEXT_ERROR_CODE",{value:"E240",enumerable:!1,configurable:!0})}} + +async function tO(e,t,r,n){if("string"!=typeof e||!e.startsWith("/"))throw Object.defineProperty(Error(\`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received \${e}\`),"__NEXT_ERROR_CODE",{value:"E153",enumerable:!1,configurable:!0});let i={[x.y3]:n.previewModeId,...t.unstable_onlyGenerated?{[x.Qq]:"1"}:{}},a=[...n.allowedRevalidateHeaderKeys||[]];for(let e of((n.trustHostHeader||n.dev)&&a.push("cookie"),n.trustHostHeader&&a.push("x-vercel-protection-bypass"),Object.keys(r.headers)))a.includes(e)&&(i[e]=r.headers[e]);let s=n.internalRevalidate;try{if(s)return await s({urlPath:e,revalidateHeaders:i,opts:t});if(r.trustHostHeader){let n=await fetch(\`\${r.headers["x-forwarded-proto"] || "https"}://\${r.headers.host}\${e}\`,{method:"HEAD",headers:i}),a=n.headers.get("x-vercel-cache")||n.headers.get("x-nextjs-cache");if((null==a?void 0:a.toUpperCase())!=="REVALIDATED"&&200!==n.status&&!(404===n.status&&t.unstable_onlyGenerated))throw Object.defineProperty(Error(\`Invalid response \${n.status}\`),"__NEXT_ERROR_CODE",{value:"E175",enumerable:!1,configurable:!0})}else throw Object.defineProperty(Error("Invariant: missing internal router-server-methods this is an internal bug"),"__NEXT_ERROR_CODE",{value:"E676",enumerable:!1,configurable:!0})}catch(t){throw Object.defineProperty(Error(\`Failed to revalidate \${e}: \${t_(t)?t.message:t}\`),"__NEXT_ERROR_CODE",{value:"E240",enumerable:!1,configurable:!0})}} + " + `); + }); + }); + + describe("unbundled res.revalidate code", () => { + it("should patch trustHostHeader", async () => { + expect( + computePatchDiff( + "pages-api.runtime.prod.js", + pagesResRevalidateCodeUnbundled, + trustHostHeaderRule, + ), + ).toMatchInlineSnapshot(` + "Index: pages-api.runtime.prod.js + =================================================================== + --- pages-api.runtime.prod.js + +++ pages-api.runtime.prod.js + @@ -1,5 +1,4 @@ + - + async function revalidate(urlPath, opts, req, context) { + if (typeof urlPath !== 'string' || !urlPath.startsWith('/')) { + throw Object.defineProperty(new Error(\`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received \${urlPath}\`), "__NEXT_ERROR_CODE", { + value: "E153", + @@ -38,9 +37,9 @@ + revalidateHeaders, + opts + }); + } + - if (context.trustHostHeader) { + + if (true) { + const res = await fetch(\`https://\${req.headers.host}\${urlPath}\`, { + method: 'HEAD', + headers: revalidateHeaders + }); + " + `); + }); + + it("should set correct protocol", async () => { + expect( + computePatchDiff( + "pages-api.runtime.prod.js", + pagesResRevalidateCodeUnbundled, + headFetchProtocolRule, + ), + ).toMatchInlineSnapshot(` + "Index: pages-api.runtime.prod.js + =================================================================== + --- pages-api.runtime.prod.js + +++ pages-api.runtime.prod.js + @@ -1,5 +1,4 @@ + - + async function revalidate(urlPath, opts, req, context) { + if (typeof urlPath !== 'string' || !urlPath.startsWith('/')) { + throw Object.defineProperty(new Error(\`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received \${urlPath}\`), "__NEXT_ERROR_CODE", { + value: "E153", + " + `); + }); + }); +}); From 2e49c07f957dcf560bf53ff8f390bfcf94734c0e Mon Sep 17 00:00:00 2001 From: magnus Date: Sat, 7 Feb 2026 00:32:00 +0100 Subject: [PATCH 2/8] changeset --- .changeset/silver-taxis-kick.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/silver-taxis-kick.md diff --git a/.changeset/silver-taxis-kick.md b/.changeset/silver-taxis-kick.md new file mode 100644 index 000000000..d4b78cfda --- /dev/null +++ b/.changeset/silver-taxis-kick.md @@ -0,0 +1,7 @@ +--- +"@opennextjs/aws": patch +--- + +fix(pages-router): Add patch for trustHostHeader using res.revalidate + +In pages router if you tried to `res.revalidate` you would run into this error: `Error: Failed to revalidate /path: Invariant: missing internal router-server-methods this is an internal bug`. This PR introduces a fix that always sets `context.trustHostHeader` as true in the runtime code. \ No newline at end of file From 3c09aaceb026986d23f255dcebb8c084ae8ad87f Mon Sep 17 00:00:00 2001 From: magnus Date: Sat, 7 Feb 2026 00:35:17 +0100 Subject: [PATCH 3/8] wording --- .changeset/silver-taxis-kick.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silver-taxis-kick.md b/.changeset/silver-taxis-kick.md index d4b78cfda..f3112bd6f 100644 --- a/.changeset/silver-taxis-kick.md +++ b/.changeset/silver-taxis-kick.md @@ -4,4 +4,4 @@ fix(pages-router): Add patch for trustHostHeader using res.revalidate -In pages router if you tried to `res.revalidate` you would run into this error: `Error: Failed to revalidate /path: Invariant: missing internal router-server-methods this is an internal bug`. This PR introduces a fix that always sets `context.trustHostHeader` as true in the runtime code. \ No newline at end of file +In pages router if you tried to `res.revalidate` you would run into this error: `Error: Failed to revalidate /path: Invariant: missing internal router-server-methods this is an internal bug`. This PR introduces a patch that always sets `context.trustHostHeader` as true in the runtime code. \ No newline at end of file From c852f60da54ac90f4d966c92d686771b40f214bc Mon Sep 17 00:00:00 2001 From: magnus Date: Sat, 7 Feb 2026 00:35:34 +0100 Subject: [PATCH 4/8] wording --- .changeset/silver-taxis-kick.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silver-taxis-kick.md b/.changeset/silver-taxis-kick.md index f3112bd6f..1b582e88a 100644 --- a/.changeset/silver-taxis-kick.md +++ b/.changeset/silver-taxis-kick.md @@ -4,4 +4,4 @@ fix(pages-router): Add patch for trustHostHeader using res.revalidate -In pages router if you tried to `res.revalidate` you would run into this error: `Error: Failed to revalidate /path: Invariant: missing internal router-server-methods this is an internal bug`. This PR introduces a patch that always sets `context.trustHostHeader` as true in the runtime code. \ No newline at end of file +In pages router on Next 15 and 16 if you tried to `res.revalidate` you would run into this error: `Error: Failed to revalidate /path: Invariant: missing internal router-server-methods this is an internal bug`. This PR introduces a patch that always sets `context.trustHostHeader` as true in the runtime code. \ No newline at end of file From 40d0aed49f54498f5478bce258bff07748e12e73 Mon Sep 17 00:00:00 2001 From: magnus Date: Sat, 7 Feb 2026 00:41:28 +0100 Subject: [PATCH 5/8] rm log --- packages/open-next/src/adapters/cache.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/open-next/src/adapters/cache.ts b/packages/open-next/src/adapters/cache.ts index 61b78f400..c830a6cf9 100644 --- a/packages/open-next/src/adapters/cache.ts +++ b/packages/open-next/src/adapters/cache.ts @@ -315,7 +315,6 @@ export default class Cache { await this.updateTagsOnSet(key, data, ctx); debug("Finished setting cache"); } catch (e) { - console.log("ARE WE HERE"); error("Failed to set cache", e); } finally { // We need to resolve the promise even if there was an error From 4097b2c2ced078edd4b70657f7aa18a22cd8566f Mon Sep 17 00:00:00 2001 From: magnus Date: Sat, 7 Feb 2026 00:48:02 +0100 Subject: [PATCH 6/8] fix comment --- .../src/build/patch/patches/patchPagesApiRuntimeProd.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/open-next/src/build/patch/patches/patchPagesApiRuntimeProd.ts b/packages/open-next/src/build/patch/patches/patchPagesApiRuntimeProd.ts index 02eff3a6e..7204defee 100644 --- a/packages/open-next/src/build/patch/patches/patchPagesApiRuntimeProd.ts +++ b/packages/open-next/src/build/patch/patches/patchPagesApiRuntimeProd.ts @@ -33,7 +33,7 @@ fix: 'true' `; -// Use correct protocol from `NextInternalRequestMeta` when doing HEAD fetch for revalidation +// Use correct protocol when doing HEAD fetch for revalidation export const headFetchProtocolRule = ` rule: kind: string_fragment @@ -80,7 +80,7 @@ export const patchPagesApiRuntimeProd: CodePatcher = { patchCode: createPatchCode(trustHostHeaderRule), versions: ">=15.0.0", }, - // Use correct protocol from `NextInternalRequestMeta` when doing HEAD fetch for revalidation + // Use correct protocol when doing HEAD fetch for revalidation { pathFilter, contentFilter: /https/, From 7df956bb041c849acf229211ce3a657ca8b63578 Mon Sep 17 00:00:00 2001 From: magnus Date: Sat, 7 Feb 2026 01:11:19 +0100 Subject: [PATCH 7/8] rm log --- packages/open-next/src/adapters/cache.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/open-next/src/adapters/cache.ts b/packages/open-next/src/adapters/cache.ts index c830a6cf9..89f42c480 100644 --- a/packages/open-next/src/adapters/cache.ts +++ b/packages/open-next/src/adapters/cache.ts @@ -207,7 +207,6 @@ export default class Cache { if (globalThis.openNextConfig.dangerous?.disableIncrementalCache) { return; } - console.log("SET CACHE", { key, data, ctx }); // This one might not even be necessary anymore // Better be safe than sorry const detachedPromise = globalThis.__openNextAls From 3492764a6656ba07fd9e418a32f2daff2234f664 Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Sat, 7 Feb 2026 20:41:22 +0100 Subject: [PATCH 8/8] review --- examples/pages-router/src/middleware.ts | 2 +- .../pages-router/src/pages/revalidate/[key].tsx | 17 +++++++++++++---- .../tests/pagesRouter/revalidate.test.ts | 17 ++++++++++++----- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/examples/pages-router/src/middleware.ts b/examples/pages-router/src/middleware.ts index 54f9e8fc5..f4b02708d 100644 --- a/examples/pages-router/src/middleware.ts +++ b/examples/pages-router/src/middleware.ts @@ -15,5 +15,5 @@ export function middleware(request: NextRequest) { } export const config = { - matcher: ["/"], + matcher: ["/", "/revalidate/:path*"], }; diff --git a/examples/pages-router/src/pages/revalidate/[key].tsx b/examples/pages-router/src/pages/revalidate/[key].tsx index 7c6be152f..5b7ccb824 100644 --- a/examples/pages-router/src/pages/revalidate/[key].tsx +++ b/examples/pages-router/src/pages/revalidate/[key].tsx @@ -13,10 +13,19 @@ type Props = { record: FakeRecord; }; -const fakeDb: FakeRecord[] = [ - { key: "1", title: "First record", updatedAt: new Date().toISOString() }, - { key: "2", title: "Second record", updatedAt: new Date().toISOString() }, - { key: "3", title: "Third record", updatedAt: new Date().toISOString() }, +const fakeDb: Omit[] = [ + { + key: "1", + title: "First record", + }, + { + key: "2", + title: "Second record", + }, + { + key: "3", + title: "Third record", + }, ]; export default function TestKeyPage({ record }: Props) { diff --git a/packages/tests-e2e/tests/pagesRouter/revalidate.test.ts b/packages/tests-e2e/tests/pagesRouter/revalidate.test.ts index d7f9e9fa1..fd1436632 100644 --- a/packages/tests-e2e/tests/pagesRouter/revalidate.test.ts +++ b/packages/tests-e2e/tests/pagesRouter/revalidate.test.ts @@ -1,11 +1,18 @@ import { expect, test } from "@playwright/test"; test("res.revalidate should work", async ({ page }) => { + // Load the page initially and get the initial updatedAt value await page.goto("/revalidate/1"); - const initialUpdatedAt = await page.getByTestId("updated-at").textContent(); + expect(initialUpdatedAt).toBe(initialUpdatedAt); + + // Reload the page again to ensure its SSG + await page.reload(); + const reloadedUpdatedAt = await page.getByTestId("updated-at").textContent(); + expect(reloadedUpdatedAt).toBe(initialUpdatedAt); - const res = await page.request.post("/api/revalidate?key=1"); + // Trigger revalidation via the API route + const res = await page.request.post("/api/revalidate/?key=1"); const json = await res.json(); expect(json).toEqual({ revalidated: true, path: "/revalidate/1" }); @@ -14,9 +21,9 @@ test("res.revalidate should work", async ({ page }) => { throw new Error(`Failed to trigger revalidation: ${await res.text()}`); } - // Wait for a short period to allow revalidation to complete - await page.waitForTimeout(1000); - await page.goto("/revalidate/1"); + // Reload the page to get the updated content after revalidation + // It should be greater than `initialUpdatedAt` + await page.reload(); const updatedUpdatedAt = await page.getByTestId("updated-at").textContent(); expect(new Date(updatedUpdatedAt!).getTime()).toBeGreaterThan(