Add PPR support and improve caching mechanisms#24
Conversation
commit: |
There was a problem hiding this comment.
Pull request overview
Adds Partial Prerendering (PPR) plumbing to OpenNext’s cache interception flow and updates Cloudflare/AWS adapters to support newer Next.js caching APIs, along with test and example updates to validate the behavior.
Changes:
- Introduces
PartialResult/initialResponseto support PPR prelude + resume-request handling across routing/request handling. - Extends cache serialization/deserialization for App Router cached entries with
postponed+segmentData. - Updates Next.js versions and adjusts e2e/unit tests and example apps to exercise PPR and composable cache behavior.
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-workspace.yaml | Bumps catalog Next.js versions (aws/e2e) to 16.2.1. |
| pnpm-lock.yaml | Lockfile updates for Next.js + transitive deps. |
| packages/tests-unit/tests/adapters/cache.test.ts | Adds unit coverage for APP_PAGE segmentData + postponed. |
| packages/tests-e2e/tests/experimental/use-cache.test.ts | Skips ISR case pending investigation. |
| packages/tests-e2e/tests/experimental/ppr.test.ts | Updates PPR expectations (postponed/prerender/cache headers). |
| packages/tests-e2e/tests/appRouter/revalidateTag.test.ts | Updates expected x-nextjs-cache behavior. |
| packages/tests-e2e/tests/appRouter/og.test.ts | Skips OG tests pending fixes. |
| packages/tests-e2e/playwright.config.js | Re-enables the experimental e2e project. |
| packages/open-next/src/types/open-next.ts | Adds PartialResult and RoutingResult.initialResponse. |
| packages/open-next/src/types/cache.ts | Adds segmentData to APP_PAGE cache value type. |
| packages/open-next/src/overrides/converters/edge.ts | Allows returning { initialResponse, request } in dangerous mode for PPR. |
| packages/open-next/src/core/routingHandler.ts | Handles PartialResult from cache interception and surfaces initialResponse. |
| packages/open-next/src/core/routing/cacheInterceptor.ts | Implements PPR cache interception/resume-request behavior and segment prefetch handling. |
| packages/open-next/src/core/requestHandler.ts | Streams initialResponse then resumes via POST (next-resume) in Node handler. |
| packages/open-next/src/adapters/middleware.ts | Propagates initialResponse through middleware adapter result. |
| packages/open-next/src/adapters/cache.ts | Writes postponed + segmentData into incremental cache storage. |
| packages/open-next/src/adapter.ts | Sets top-level cacheHandlers (default/remote) for composable cache. |
| packages/cloudflare/src/cli/templates/worker.ts | Adds Cloudflare worker handling for PPR prelude + resume streaming. |
| packages/cloudflare/src/cli/build/patches/plugins/route-module.ts | Patches Next route-module to require composable cache handler module. |
| packages/cloudflare/src/cli/build/patches/plugins/route-module.spec.ts | Notes missing test coverage for the new composable cache patch. |
| packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts | Treats certain manifests as optional to avoid worker crashes. |
| packages/cloudflare/src/cli/adapter.ts | Sets top-level cacheHandlers (default/remote) for composable cache. |
| examples/experimental/tsconfig.json | Switches JSX mode and includes additional .next dev types. |
| examples/experimental/src/proxy.ts | Renames default export and removes explicit runtime config. |
| examples/experimental/src/components/cached.tsx | Updates to cacheLife/cacheTag APIs and sets cache lifetime. |
| examples/experimental/src/app/ppr/page.tsx | Removes experimental_ppr flag export. |
| examples/experimental/src/app/api/revalidate/route.ts | Adjusts revalidateTag usage in the example. |
| examples/experimental/package.json | Updates example to Next 16.2 canary + React 19.2.4 and adds local scripts/overrides. |
| examples/experimental/open-next.config.ts | Consolidates local/dev OpenNext config and enables cache interception. |
| examples/experimental/open-next.config.local.ts | Removes local config file (merged into main config). |
| examples/experimental/next.config.ts | Updates Next config to use cacheComponents. |
| examples-cloudflare/e2e/experimental/package.json | Renames CF scripts to *:cf. |
| examples-cloudflare/e2e/experimental/open-next.config.ts | Enables cache interception for CF e2e experimental app. |
| examples-cloudflare/e2e/experimental/e2e/use-cache.test.ts | Skips ISR-related cache tests pending investigation. |
| examples-cloudflare/e2e/experimental/e2e/ppr.test.ts | Updates PPR expectations to match new header behavior. |
| examples-cloudflare/e2e/app-router/e2e/revalidateTag.test.ts | Updates expected x-nextjs-cache behavior. |
| examples-cloudflare/e2e/app-router/e2e/og.test.ts | Skips OG test pending fixes. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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(pathToUse); |
There was a problem hiding this comment.
PrerenderManifest.dynamicRoutes[...].fallback can be string | false | null (per types). Casting with ?.[1].fallback! as string can produce false/null/undefined, which then gets passed to incrementalCache.get(pathToUse) and can throw or create invalid cache keys for dynamic ISR routes. Guard this by checking typeof fallback === "string" before using it, and fall back to localizedPath (or return event) when fallback is not a string.
| } 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, | ||
| }; |
There was a problem hiding this comment.
In the PartialResult path, middleware headers are applied to cacheInterceptionResult.result (an InternalResult). applyMiddlewareHeaders currently flattens string[] values via .join(","), which breaks multi-value headers like Set-Cookie (commas are not a valid delimiter there). For PPR initial responses this can corrupt cookies. Consider updating the middleware-header application logic to preserve string[] values on InternalResult.headers (at least for set-cookie) instead of joining them.
| headers.set("content-encoding", "identity"); // To fix PPR locally | ||
|
|
||
| return new Response(body, { | ||
| status: reqOrResp.initialResponse.statusCode, | ||
| headers: headers, | ||
| }); |
There was a problem hiding this comment.
headers.set("content-encoding", "identity") is applied unconditionally for all PPR responses. This disables compression and may negatively affect bandwidth/caching behavior in production. If this is only needed for wrangler dev/localhost, gate it on the request hostname or an env flag (similar to the conditional workaround in cloudflare-node wrapper).
| test("patch the createSnapshot function", () => { | ||
| //TODO: add the test for the composable cache handler | ||
| expect( |
There was a problem hiding this comment.
createComposableCacheHandlersRule is introduced/used in the patcher, but there’s currently no assertion covering it (note the TODO). Adding a targeted test that snapshots the diff produced by createComposableCacheHandlersRule would help prevent silent breakage across Next.js versions/minified output changes.
Add PPR support in both cloudflare and aws.
Left to do :
use cachein cloudflareFigure out whyLet's do that in a follow up PRuse cachedoesn't work in ISR