From e2572a31d92fa0eea46bf5e2a41e1fae44a54601 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Thu, 13 Nov 2025 19:54:51 +0100 Subject: [PATCH 1/4] wip --- .../src/core/routing/adapterHandler.ts | 10 +++ .../src/core/routing/cacheInterceptor.ts | 75 +++++++++++++++---- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/packages/open-next/src/core/routing/adapterHandler.ts b/packages/open-next/src/core/routing/adapterHandler.ts index c6c0125aa..6548002ac 100644 --- a/packages/open-next/src/core/routing/adapterHandler.ts +++ b/packages/open-next/src/core/routing/adapterHandler.ts @@ -3,6 +3,8 @@ import { finished } from "node:stream/promises"; import type { OpenNextNodeResponse } from "http/index"; import type { ResolvedRoute, RoutingResult, WaitUntil } from "types/open-next"; +const NEXT_REQUEST_META = Symbol.for('NextInternalRequestMeta') + /** * This function loads the necessary routes, and invoke the expected handler. * @param routingResult The result of the routing process, containing information about the matched route and any parameters. @@ -16,6 +18,14 @@ export async function adapterHandler( } = {}, ) { let resolved = false; + if (routingResult.internalEvent.headers['next-resume'] === "1") { + + const postponed = routingResult.internalEvent.body!.toString('utf8') + // @ts-expect-error + req[NEXT_REQUEST_META] = { + postponed + } + } //TODO: replace this at runtime with a version precompiled for the cloudflare adapter. for (const route of routingResult.resolvedRoutes) { diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts index 28577db1e..8b424814d 100644 --- a/packages/open-next/src/core/routing/cacheInterceptor.ts +++ b/packages/open-next/src/core/routing/cacheInterceptor.ts @@ -141,7 +141,18 @@ async function generateResult( lastModified?: number, ): Promise { debug("Returning result from experimental cache"); - let body = ""; + let enqueue = (chunk: any) => {}; + let close = () => {}; + const readableBody = new ReadableStream({ + start(controller) { + enqueue = (chunk: any) => { + controller.enqueue(chunk); + }; + close = () => { + controller.close(); + }; + } + }); let type = "application/octet-stream"; let isDataRequest = false; let additionalHeaders = {}; @@ -150,28 +161,62 @@ async function generateResult( if (isDataRequest) { const { body: appRouterBody, additionalHeaders: appHeaders } = getBodyForAppRouter(event, cachedValue); - body = appRouterBody; + enqueue(appRouterBody); additionalHeaders = appHeaders; } else { - body = cachedValue.html; + if(cachedValue.meta?.postponed) { + console.log("Postponed request detected", localizedPath); + const formData = new FormData(); + formData.append('path', localizedPath); + const result = fetch(`http://localhost:3000/${localizedPath}`, { + method: 'POST', + headers: { + 'next-resume': '1', + 'x-matched-path': localizedPath + }, + body: cachedValue.meta?.postponed + }); + // const data = await result.text() + enqueue(cachedValue.html); + result.then(text => { + text.body!.getReader().read().then(({ done, value }) => { + if (done) { + console.log("Stream finished"); + close(); + return; + } + enqueue(value); + }); + }).catch(err => { + console.error(err); + }); + } else { + enqueue(cachedValue.html); + close(); + } } type = isDataRequest ? "text/x-component" : "text/html; charset=utf-8"; } else if (cachedValue.type === "page") { isDataRequest = Boolean(event.query.__nextDataReq); - body = isDataRequest ? JSON.stringify(cachedValue.json) : cachedValue.html; + if (isDataRequest) { + enqueue(JSON.stringify(cachedValue.json)); + } else { + enqueue(cachedValue.html); + } type = isDataRequest ? "application/json" : "text/html; charset=utf-8"; } else { throw new Error( "generateResult called with unsupported cache value type, only 'app' and 'page' are supported", ); } - const cacheControl = await computeCacheControl( - localizedPath, - body, - event.headers.host, - cachedValue.revalidate, - lastModified, - ); + // close(); + // const cacheControl = await computeCacheControl( + // localizedPath, + // , + // event.headers.host, + // cachedValue.revalidate, + // lastModified, + // ); return { type: "core", // Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code @@ -180,10 +225,11 @@ async function generateResult( // `NextResponse.rewrite(url, { status: xxx}) // The rewrite status code should take precedence over the cached one statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, - body: toReadableStream(body, false), + // @ts-expect-error + body: readableBody, isBase64Encoded: false, headers: { - ...cacheControl, + // ...cacheControl, "content-type": type, ...cachedValue.meta?.headers, vary: VARY_HEADER, @@ -230,7 +276,8 @@ export async function cacheInterceptor( ): Promise { if ( Boolean(event.headers["next-action"]) || - Boolean(event.headers["x-prerender-revalidate"]) + Boolean(event.headers["x-prerender-revalidate"]) || + Boolean(event.headers["next-resume"]) ) return event; From fa92ae4e2e4f878fa1d90a6bf2718fe3d10057ea Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sun, 7 Dec 2025 17:23:27 +0100 Subject: [PATCH 2/4] make ppr work --- packages/open-next/src/core/requestHandler.ts | 49 ++++- .../src/core/routing/adapterHandler.ts | 17 +- .../src/core/routing/cacheInterceptor.ts | 172 ++++++++++++------ packages/open-next/src/core/routingHandler.ts | 51 ++++-- packages/open-next/src/types/open-next.ts | 25 +++ 5 files changed, 226 insertions(+), 88 deletions(-) diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index b3857b0b9..4ed33a86c 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -31,6 +31,8 @@ import routingHandler, { MIDDLEWARE_HEADER_PREFIX_LEN, } from "./routingHandler"; import { requestHandler, setNextjsPrebundledReact } from "./util"; +import { Writable } from "node:stream"; +import { finished } from "node:stream/promises"; // This is used to identify requests in the cache globalThis.__openNextAls = new AsyncLocalStorage(); @@ -91,10 +93,7 @@ export async function openNextHandler( }); //#endOverride - const headers = - "type" in routingResult - ? routingResult.headers - : routingResult.internalEvent.headers; + const headers = getHeaders(routingResult); const overwrittenResponseHeaders: Record = {}; @@ -205,10 +204,42 @@ export async function openNextHandler( const req = new IncomingMessage(reqProps); const res = createServerResponse( routingResult, - overwrittenResponseHeaders, + routingResult.initialResponse ? routingResult.initialResponse.headers : overwrittenResponseHeaders, options?.streamCreator, ); + if(routingResult.initialResponse) { + res.statusCode = routingResult.initialResponse.statusCode; + res.flushHeaders(); + for await (const chunk of routingResult.initialResponse.body) { + res.write(chunk); + } + + //We create a special response for the PPR resume request + const pprRes = createServerResponse( + routingResult, + overwrittenResponseHeaders, + { + writeHeaders: () => { + return new Writable({ + write(chunk, encoding, callback) { + res.write(chunk, encoding, callback); + }, + + }) + } + } + ); + await adapterHandler(req, pprRes, routingResult, { + waitUntil: options?.waitUntil, + }); + await finished(pprRes); + res.end(); + + return convertRes(res); + + } + //#override useAdapterHandler await adapterHandler(req, res, routingResult, { waitUntil: options?.waitUntil, @@ -239,6 +270,14 @@ export async function openNextHandler( ); } +function getHeaders(routingResult: RoutingResult | InternalResult) { + if("type" in routingResult) { + return routingResult.headers; + } else { + return routingResult.internalEvent.headers; + } +} + async function processRequest( req: IncomingMessage, res: OpenNextNodeResponse, diff --git a/packages/open-next/src/core/routing/adapterHandler.ts b/packages/open-next/src/core/routing/adapterHandler.ts index 6548002ac..02978ce3d 100644 --- a/packages/open-next/src/core/routing/adapterHandler.ts +++ b/packages/open-next/src/core/routing/adapterHandler.ts @@ -2,8 +2,7 @@ import type { IncomingMessage } from "node:http"; import { finished } from "node:stream/promises"; import type { OpenNextNodeResponse } from "http/index"; import type { ResolvedRoute, RoutingResult, WaitUntil } from "types/open-next"; - -const NEXT_REQUEST_META = Symbol.for('NextInternalRequestMeta') +import { debug, error } from "../../adapters/logger"; /** * This function loads the necessary routes, and invoke the expected handler. @@ -18,14 +17,6 @@ export async function adapterHandler( } = {}, ) { let resolved = false; - if (routingResult.internalEvent.headers['next-resume'] === "1") { - - const postponed = routingResult.internalEvent.body!.toString('utf8') - // @ts-expect-error - req[NEXT_REQUEST_META] = { - postponed - } - } //TODO: replace this at runtime with a version precompiled for the cloudflare adapter. for (const route of routingResult.resolvedRoutes) { @@ -35,17 +26,17 @@ export async function adapterHandler( } try { - console.log("## adapterHandler trying route", route, req.url); + debug("## adapterHandler trying route", route, req.url); const result = await module.handler(req, res, { waitUntil: options.waitUntil, }); await finished(res); // Not sure this one is necessary. - console.log("## adapterHandler route succeeded", route); + debug("## adapterHandler route succeeded", route); resolved = true; return result; //If it doesn't throw, we are done } catch (e) { - console.log("## adapterHandler route failed", route, e); + error("## adapterHandler route failed", route, e); // I'll have to run some more tests, but in theory, we should not have anything special to do here, and we should return the 500 page here. } } diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts index 8b424814d..b34ac29bc 100644 --- a/packages/open-next/src/core/routing/cacheInterceptor.ts +++ b/packages/open-next/src/core/routing/cacheInterceptor.ts @@ -5,6 +5,7 @@ import type { InternalEvent, InternalResult, MiddlewareEvent, + PartialResult, } from "types/open-next"; import type { CacheValue } from "types/overrides"; import { emptyReadableStream, toReadableStream } from "utils/stream"; @@ -139,69 +140,95 @@ async function generateResult( localizedPath: string, cachedValue: CacheValue<"cache">, lastModified?: number, -): Promise { +): Promise { debug("Returning result from experimental cache"); - let enqueue = (chunk: any) => {}; - let close = () => {}; - const readableBody = new ReadableStream({ - start(controller) { - enqueue = (chunk: any) => { - controller.enqueue(chunk); - }; - close = () => { - controller.close(); - }; - } - }); let type = "application/octet-stream"; let isDataRequest = false; let additionalHeaders = {}; + let body: string; if (cachedValue.type === "app") { isDataRequest = Boolean(event.headers.rsc); if (isDataRequest) { const { body: appRouterBody, additionalHeaders: appHeaders } = getBodyForAppRouter(event, cachedValue); - enqueue(appRouterBody); + body = appRouterBody; additionalHeaders = appHeaders; + if(cachedValue.meta?.postponed) { + debug("App router postponed request detected", localizedPath); + + return { + resumeRequest: { + ...event, + method: "POST", + url: `http://${event.headers.host}${NextConfig.basePath || ""}${ + localizedPath || "/" + }`, + headers: { + ...event.headers, + "next-resume": "1", + }, + rawPath: localizedPath, + body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8") + }, + result: { + type: "core", + statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, + // It doesn't want to build for some reasons + body: emptyReadableStream(), + isBase64Encoded: false, + headers: { + "content-type": "text/x-component", + "x-opennext-ppr": "1", + ...cachedValue.meta?.headers, + vary: VARY_HEADER, + }, + }, + } satisfies PartialResult; + } + debug("App router data request detected", localizedPath, body); } else { if(cachedValue.meta?.postponed) { - console.log("Postponed request detected", localizedPath); - const formData = new FormData(); - formData.append('path', localizedPath); - const result = fetch(`http://localhost:3000/${localizedPath}`, { - method: 'POST', - headers: { - 'next-resume': '1', - 'x-matched-path': localizedPath + debug("Postponed request detected", localizedPath); + + return { + resumeRequest: { + ...event, + method: "POST", + url: `http://${event.headers.host}${NextConfig.basePath || ""}${ + localizedPath || "/" + }`, + headers: { + ...event.headers, + "next-resume": "1", + }, + rawPath: localizedPath, + body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8") + }, + result: { + type: "core", + statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, + // It doesn't want to build for some reasons + body: toReadableStream(cachedValue.html), + isBase64Encoded: false, + headers: { + "content-type": "text/html; charset=utf-8", + "x-opennext-ppr": "1", + ...cachedValue.meta?.headers, + vary: VARY_HEADER, + }, }, - body: cachedValue.meta?.postponed - }); - // const data = await result.text() - enqueue(cachedValue.html); - result.then(text => { - text.body!.getReader().read().then(({ done, value }) => { - if (done) { - console.log("Stream finished"); - close(); - return; - } - enqueue(value); - }); - }).catch(err => { - console.error(err); - }); + } satisfies PartialResult; } else { - enqueue(cachedValue.html); - close(); + body = cachedValue.html; } } type = isDataRequest ? "text/x-component" : "text/html; charset=utf-8"; } else if (cachedValue.type === "page") { isDataRequest = Boolean(event.query.__nextDataReq); if (isDataRequest) { - enqueue(JSON.stringify(cachedValue.json)); + body = JSON.stringify(cachedValue.json); } else { - enqueue(cachedValue.html); + body = cachedValue.html; } type = isDataRequest ? "application/json" : "text/html; charset=utf-8"; } else { @@ -210,13 +237,13 @@ async function generateResult( ); } // close(); - // const cacheControl = await computeCacheControl( - // localizedPath, - // , - // event.headers.host, - // cachedValue.revalidate, - // lastModified, - // ); + const cacheControl = await computeCacheControl( + localizedPath, + body, + event.headers.host, + cachedValue.revalidate, + lastModified, + ); return { type: "core", // Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code @@ -225,11 +252,10 @@ async function generateResult( // `NextResponse.rewrite(url, { status: xxx}) // The rewrite status code should take precedence over the cached one statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, - // @ts-expect-error - body: readableBody, + body: toReadableStream(body, isBinaryContentType(type)), isBase64Encoded: false, headers: { - // ...cacheControl, + ...cacheControl, "content-type": type, ...cachedValue.meta?.headers, vary: VARY_HEADER, @@ -273,13 +299,19 @@ function decodePathParams(pathname: string): string { export async function cacheInterceptor( event: MiddlewareEvent, -): Promise { +): Promise { if ( Boolean(event.headers["next-action"]) || Boolean(event.headers["x-prerender-revalidate"]) || - Boolean(event.headers["next-resume"]) + Boolean(event.headers["next-resume"]) || + event.method !== "GET" ) return event; + + // if(Boolean(event.headers.rsc) && !(Boolean(event.headers["next-router-prefetch"]) || Boolean(event.headers[NEXT_SEGMENT_PREFETCH_HEADER]))) { + // // Let the handler deal with RSC requests with no prefetch header as they are SSR requests + // return event; + // } // Check for Next.js preview mode cookies const cookies = event.headers.cookie || ""; @@ -305,16 +337,38 @@ export async function cacheInterceptor( debug("Checking cache for", localizedPath, PrerenderManifest); + const isDynamicISR = Object.values( + PrerenderManifest.dynamicRoutes, + ).some((dr) => { + const regex = new RegExp(dr.routeRegex); + return regex.test(localizedPath); + }); + + const isStaticRoute = Object.keys(PrerenderManifest.routes).includes( + localizedPath || "/", + ); + const isISR = - Object.keys(PrerenderManifest.routes).includes(localizedPath ?? "/") || - Object.values(PrerenderManifest.dynamicRoutes).some((dr) => - new RegExp(dr.routeRegex).test(localizedPath), - ); + isStaticRoute || + isDynamicISR; debug("isISR", isISR); if (isISR) { try { + let pathToUse = localizedPath; + // For PPR, we need to check the fallback value to get the correct cache key + // We don't want to override a static route though + if (isDynamicISR && !isStaticRoute) { + pathToUse = Object.entries( + PrerenderManifest.dynamicRoutes, + ).find(([, dr]) => { + const regex = new RegExp(dr.routeRegex); + return regex.test(localizedPath); + })?.[1].fallback! as string + }else if (localizedPath === "") { + pathToUse = "/index"; + } const cachedData = await globalThis.incrementalCache.get( - localizedPath ?? "/index", + pathToUse ); debug("cached data in interceptor", cachedData); diff --git a/packages/open-next/src/core/routingHandler.ts b/packages/open-next/src/core/routingHandler.ts index 951be07c5..23d28ae45 100644 --- a/packages/open-next/src/core/routingHandler.ts +++ b/packages/open-next/src/core/routingHandler.ts @@ -8,6 +8,7 @@ import { import type { InternalEvent, InternalResult, + PartialResult, ResolvedRoute, RoutingResult, } from "types/open-next"; @@ -235,26 +236,44 @@ export default async function routingHandler( }; } + const resolvedRoutes: ResolvedRoute[] = [ + ...foundStaticRoute, + ...foundDynamicRoute, + ]; + if ( globalThis.openNextConfig.dangerous?.enableCacheInterception && !isInternalResult(eventOrResult) ) { debug("Cache interception enabled"); - eventOrResult = await cacheInterceptor(eventOrResult); - if (isInternalResult(eventOrResult)) { - applyMiddlewareHeaders(eventOrResult, headers); - return eventOrResult; + const cacheInterceptionResult = await cacheInterceptor(eventOrResult); + if (isInternalResult(cacheInterceptionResult)) { + applyMiddlewareHeaders(cacheInterceptionResult, headers); + return cacheInterceptionResult; + }else if (isPartialResult(cacheInterceptionResult)) { + // We need to apply the headers to both the result (the streamed response) and the resume request + applyMiddlewareHeaders(cacheInterceptionResult.result, headers); + applyMiddlewareHeaders(cacheInterceptionResult.resumeRequest, headers); + return { + internalEvent: cacheInterceptionResult.resumeRequest, + isExternalRewrite: false, + origin: false, + isISR: false, + resolvedRoutes, + initialURL: event.url, + locale: NextConfig.i18n + ? detectLocale(eventOrResult, NextConfig.i18n) + : undefined, + rewriteStatusCode: + middlewareEventOrResult.rewriteStatusCode, + initialResponse: cacheInterceptionResult.result, + }; } } // We apply the headers from the middleware response last applyMiddlewareHeaders(eventOrResult, headers); - const resolvedRoutes: ResolvedRoute[] = [ - ...foundStaticRoute, - ...foundDynamicRoute, - ]; - debug("resolvedRoutes", resolvedRoutes); return { @@ -301,8 +320,18 @@ export default async function routingHandler( * @param eventOrResult * @returns Whether the event is an instance of `InternalResult` */ -function isInternalResult( - eventOrResult: InternalEvent | InternalResult, +export function isInternalResult( + eventOrResult: InternalEvent | InternalResult | PartialResult, ): eventOrResult is InternalResult { return eventOrResult != null && "statusCode" in eventOrResult; } + +/** + * @param eventOrResult + * @returns Whether the event is an instance of `PartialResult` (i.e. for PPR responses) + */ +export function isPartialResult( + eventOrResult: InternalEvent | InternalResult | PartialResult, +): eventOrResult is PartialResult { + return eventOrResult != null && "resumeRequest" in eventOrResult; +} \ No newline at end of file diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index 9241cd50c..8560ac729 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -38,6 +38,24 @@ export type MiddlewareEvent = InternalEvent & { rewriteStatusCode?: number; }; +/** + * This event is returned by the cache interceptor and the routing handler. + * It is then handled by either the external middleware or the classic request handler. + * This is designed for PPR support inside the cache interceptor. + */ +export type PartialResult = { + /** + * Resume request that will be forwarded to the handler + */ + resumeRequest: InternalEvent, + /** + * The result that was generated so far by the cache interceptor + * It contains the first part of the body that we'll need to forward to the client immediately + * As well as the headers and status code + */ + result: InternalResult +} + export type InternalResult = { statusCode: number; headers: Record; @@ -192,6 +210,13 @@ export interface RoutingResult { resolvedRoutes: ResolvedRoute[]; // The status code applied to a middleware rewrite rewriteStatusCode?: number; + + /** + * This is the response generated when using PPR in the cache interceptor. + * It contains the initial part of the response that should be sent to the client immediately. + * Can only be present when using cache interception and no external middleware. + */ + initialResponse?: InternalResult; } export interface MiddlewareResult From 9f7cb21b657d9051f86a90bd8f2529e38b92365e Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sun, 7 Dec 2025 18:35:44 +0100 Subject: [PATCH 3/4] changeset and linting --- .changeset/spicy-laws-fold.md | 5 ++ packages/open-next/src/core/requestHandler.ts | 20 +++---- .../src/core/routing/cacheInterceptor.ts | 52 +++++++++---------- packages/open-next/src/core/routingHandler.ts | 7 ++- packages/open-next/src/types/open-next.ts | 8 +-- 5 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 .changeset/spicy-laws-fold.md diff --git a/.changeset/spicy-laws-fold.md b/.changeset/spicy-laws-fold.md new file mode 100644 index 000000000..0f722de7a --- /dev/null +++ b/.changeset/spicy-laws-fold.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": minor +--- + +Make PPR work with the cache interceptor diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index 4ed33a86c..f99b5bfa9 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -10,6 +10,8 @@ import type { } from "types/open-next"; import { runWithOpenNextRequestContext } from "utils/promise"; +import { Writable } from "node:stream"; +import { finished } from "node:stream/promises"; import { NextConfig } from "config/index"; import type { OpenNextHandlerOptions } from "types/overrides"; import { debug, error } from "../adapters/logger"; @@ -31,8 +33,6 @@ import routingHandler, { MIDDLEWARE_HEADER_PREFIX_LEN, } from "./routingHandler"; import { requestHandler, setNextjsPrebundledReact } from "./util"; -import { Writable } from "node:stream"; -import { finished } from "node:stream/promises"; // This is used to identify requests in the cache globalThis.__openNextAls = new AsyncLocalStorage(); @@ -204,11 +204,13 @@ export async function openNextHandler( const req = new IncomingMessage(reqProps); const res = createServerResponse( routingResult, - routingResult.initialResponse ? routingResult.initialResponse.headers : overwrittenResponseHeaders, + routingResult.initialResponse + ? routingResult.initialResponse.headers + : overwrittenResponseHeaders, options?.streamCreator, ); - if(routingResult.initialResponse) { + if (routingResult.initialResponse) { res.statusCode = routingResult.initialResponse.statusCode; res.flushHeaders(); for await (const chunk of routingResult.initialResponse.body) { @@ -225,10 +227,9 @@ export async function openNextHandler( write(chunk, encoding, callback) { res.write(chunk, encoding, callback); }, - - }) - } - } + }); + }, + }, ); await adapterHandler(req, pprRes, routingResult, { waitUntil: options?.waitUntil, @@ -237,7 +238,6 @@ export async function openNextHandler( res.end(); return convertRes(res); - } //#override useAdapterHandler @@ -271,7 +271,7 @@ export async function openNextHandler( } function getHeaders(routingResult: RoutingResult | InternalResult) { - if("type" in routingResult) { + if ("type" in routingResult) { return routingResult.headers; } else { return routingResult.internalEvent.headers; diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts index df49b269c..1b17b4796 100644 --- a/packages/open-next/src/core/routing/cacheInterceptor.ts +++ b/packages/open-next/src/core/routing/cacheInterceptor.ts @@ -153,9 +153,9 @@ async function generateResult( getBodyForAppRouter(event, cachedValue); body = appRouterBody; additionalHeaders = appHeaders; - if(cachedValue.meta?.postponed) { + if (cachedValue.meta?.postponed) { debug("App router postponed request detected", localizedPath); - + return { resumeRequest: { ...event, @@ -168,11 +168,12 @@ async function generateResult( "next-resume": "1", }, rawPath: localizedPath, - body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8") + body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8"), }, result: { type: "core", - statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, + statusCode: + event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, // It doesn't want to build for some reasons body: emptyReadableStream(), isBase64Encoded: false, @@ -187,7 +188,7 @@ async function generateResult( } debug("App router data request detected", localizedPath, body); } else { - if(cachedValue.meta?.postponed) { + if (cachedValue.meta?.postponed) { debug("Postponed request detected", localizedPath); return { @@ -202,11 +203,12 @@ async function generateResult( "next-resume": "1", }, rawPath: localizedPath, - body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8") + body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8"), }, result: { type: "core", - statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, + statusCode: + event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, // It doesn't want to build for some reasons body: toReadableStream(cachedValue.html), isBase64Encoded: false, @@ -307,7 +309,7 @@ export async function cacheInterceptor( event.method !== "GET" ) return event; - + // if(Boolean(event.headers.rsc) && !(Boolean(event.headers["next-router-prefetch"]) || Boolean(event.headers[NEXT_SEGMENT_PREFETCH_HEADER]))) { // // Let the handler deal with RSC requests with no prefetch header as they are SSR requests // return event; @@ -337,20 +339,18 @@ export async function cacheInterceptor( debug("Checking cache for", localizedPath, PrerenderManifest); - const isDynamicISR = Object.values( - PrerenderManifest.dynamicRoutes, - ).some((dr) => { - const regex = new RegExp(dr.routeRegex); - return regex.test(localizedPath); - }); + const isDynamicISR = Object.values(PrerenderManifest.dynamicRoutes).some( + (dr) => { + const regex = new RegExp(dr.routeRegex); + return regex.test(localizedPath); + }, + ); const isStaticRoute = Object.keys(PrerenderManifest.routes).includes( localizedPath || "/", ); - const isISR = - isStaticRoute || - isDynamicISR; + const isISR = isStaticRoute || isDynamicISR; debug("isISR", isISR); if (isISR) { try { @@ -358,18 +358,16 @@ export async function cacheInterceptor( // For PPR, we need to check the fallback value to get the correct cache key // We don't want to override a static route though if (isDynamicISR && !isStaticRoute) { - pathToUse = Object.entries( - PrerenderManifest.dynamicRoutes, - ).find(([, dr]) => { - const regex = new RegExp(dr.routeRegex); - return regex.test(localizedPath); - })?.[1].fallback! as string - }else if (localizedPath === "") { + pathToUse = Object.entries(PrerenderManifest.dynamicRoutes).find( + ([, dr]) => { + const regex = new RegExp(dr.routeRegex); + return regex.test(localizedPath); + }, + )?.[1].fallback! as string; + } else if (localizedPath === "") { pathToUse = "/index"; } - const cachedData = await globalThis.incrementalCache.get( - pathToUse - ); + const cachedData = await globalThis.incrementalCache.get(pathToUse); debug("cached data in interceptor", cachedData); if (!cachedData?.value) { diff --git a/packages/open-next/src/core/routingHandler.ts b/packages/open-next/src/core/routingHandler.ts index 23d28ae45..9b429d48a 100644 --- a/packages/open-next/src/core/routingHandler.ts +++ b/packages/open-next/src/core/routingHandler.ts @@ -250,7 +250,7 @@ export default async function routingHandler( if (isInternalResult(cacheInterceptionResult)) { applyMiddlewareHeaders(cacheInterceptionResult, headers); return cacheInterceptionResult; - }else if (isPartialResult(cacheInterceptionResult)) { + } else if (isPartialResult(cacheInterceptionResult)) { // We need to apply the headers to both the result (the streamed response) and the resume request applyMiddlewareHeaders(cacheInterceptionResult.result, headers); applyMiddlewareHeaders(cacheInterceptionResult.resumeRequest, headers); @@ -264,8 +264,7 @@ export default async function routingHandler( locale: NextConfig.i18n ? detectLocale(eventOrResult, NextConfig.i18n) : undefined, - rewriteStatusCode: - middlewareEventOrResult.rewriteStatusCode, + rewriteStatusCode: middlewareEventOrResult.rewriteStatusCode, initialResponse: cacheInterceptionResult.result, }; } @@ -334,4 +333,4 @@ export function isPartialResult( eventOrResult: InternalEvent | InternalResult | PartialResult, ): eventOrResult is PartialResult { return eventOrResult != null && "resumeRequest" in eventOrResult; -} \ No newline at end of file +} diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index 8560ac729..ec709ff0d 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -47,14 +47,14 @@ export type PartialResult = { /** * Resume request that will be forwarded to the handler */ - resumeRequest: InternalEvent, + resumeRequest: InternalEvent; /** * The result that was generated so far by the cache interceptor * It contains the first part of the body that we'll need to forward to the client immediately - * As well as the headers and status code + * As well as the headers and status code */ - result: InternalResult -} + result: InternalResult; +}; export type InternalResult = { statusCode: number; From a2e94ff343afce1a5a2fc5871c8bc41a43e5ac2a Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sun, 7 Dec 2025 22:10:40 +0100 Subject: [PATCH 4/4] review --- .../src/core/routing/cacheInterceptor.ts | 133 +++++++++--------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts index 1b17b4796..13d4d50d6 100644 --- a/packages/open-next/src/core/routing/cacheInterceptor.ts +++ b/packages/open-next/src/core/routing/cacheInterceptor.ts @@ -135,6 +135,49 @@ function getBodyForAppRouter( } } +function createPprPartialResult( + event: MiddlewareEvent, + localizedPath: string, + cachedValue: CacheValue<"cache">, + responseBody: string | (() => ReturnType), + contentType: string, +): PartialResult { + if (cachedValue.type !== "app") { + throw new Error("createPprPartialResult called with non-app cache value"); + } + + return { + resumeRequest: { + ...event, + method: "POST", + url: `http://${event.headers.host}${NextConfig.basePath || ""}${ + localizedPath || "/" + }`, + headers: { + ...event.headers, + "next-resume": "1", + }, + rawPath: localizedPath, + body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8"), + }, + result: { + type: "core", + statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, + body: + typeof responseBody === "string" + ? toReadableStream(responseBody) + : responseBody(), + isBase64Encoded: false, + headers: { + "content-type": contentType, + "x-opennext-ppr": "1", + ...cachedValue.meta?.headers, + vary: VARY_HEADER, + }, + }, + }; +} + async function generateResult( event: MiddlewareEvent, localizedPath: string, @@ -155,71 +198,25 @@ async function generateResult( additionalHeaders = appHeaders; if (cachedValue.meta?.postponed) { debug("App router postponed request detected", localizedPath); - - return { - resumeRequest: { - ...event, - method: "POST", - url: `http://${event.headers.host}${NextConfig.basePath || ""}${ - localizedPath || "/" - }`, - headers: { - ...event.headers, - "next-resume": "1", - }, - rawPath: localizedPath, - body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8"), - }, - result: { - type: "core", - statusCode: - event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, - // It doesn't want to build for some reasons - body: emptyReadableStream(), - isBase64Encoded: false, - headers: { - "content-type": "text/x-component", - "x-opennext-ppr": "1", - ...cachedValue.meta?.headers, - vary: VARY_HEADER, - }, - }, - } satisfies PartialResult; + return createPprPartialResult( + event, + localizedPath, + cachedValue, + () => emptyReadableStream(), + "text/x-component", + ); } debug("App router data request detected", localizedPath, body); } else { if (cachedValue.meta?.postponed) { debug("Postponed request detected", localizedPath); - - return { - resumeRequest: { - ...event, - method: "POST", - url: `http://${event.headers.host}${NextConfig.basePath || ""}${ - localizedPath || "/" - }`, - headers: { - ...event.headers, - "next-resume": "1", - }, - rawPath: localizedPath, - body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8"), - }, - result: { - type: "core", - statusCode: - event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, - // It doesn't want to build for some reasons - body: toReadableStream(cachedValue.html), - isBase64Encoded: false, - headers: { - "content-type": "text/html; charset=utf-8", - "x-opennext-ppr": "1", - ...cachedValue.meta?.headers, - vary: VARY_HEADER, - }, - }, - } satisfies PartialResult; + return createPprPartialResult( + event, + localizedPath, + cachedValue, + cachedValue.html, + "text/html; charset=utf-8", + ); } else { body = cachedValue.html; } @@ -339,14 +336,14 @@ export async function cacheInterceptor( debug("Checking cache for", localizedPath, PrerenderManifest); - const isDynamicISR = Object.values(PrerenderManifest.dynamicRoutes).some( - (dr) => { - const regex = new RegExp(dr.routeRegex); - return regex.test(localizedPath); - }, - ); + const isDynamicISR = Object.values( + PrerenderManifest?.dynamicRoutes ?? {}, + ).some((dr) => { + const regex = new RegExp(dr.routeRegex); + return regex.test(localizedPath); + }); - const isStaticRoute = Object.keys(PrerenderManifest.routes).includes( + const isStaticRoute = Object.keys(PrerenderManifest?.routes ?? {}).includes( localizedPath || "/", ); @@ -358,7 +355,7 @@ export async function cacheInterceptor( // For PPR, we need to check the fallback value to get the correct cache key // We don't want to override a static route though if (isDynamicISR && !isStaticRoute) { - pathToUse = Object.entries(PrerenderManifest.dynamicRoutes).find( + pathToUse = Object.entries(PrerenderManifest?.dynamicRoutes ?? {}).find( ([, dr]) => { const regex = new RegExp(dr.routeRegex); return regex.test(localizedPath);