From 0770f28534c6992db55054813d8469c5615f300c Mon Sep 17 00:00:00 2001 From: David Alexandru Ilie Date: Sun, 9 Aug 2026 00:37:26 +0200 Subject: [PATCH 1/3] Stop app route prerenders after caught request access --- .../server/route-modules/app-route/module.ts | 7 +++++-- .../app/routes/caught-dynamic-url/route.ts | 20 +++++++++++++++++++ .../cache-components.routes.test.ts | 16 +++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 test/e2e/app-dir/cache-components/app/routes/caught-dynamic-url/route.ts diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index 2110a4a6f3f6..0c7c9fa14a10 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -637,10 +637,13 @@ export class AppRouteRouteModule extends RouteModule< trackPendingModules(cacheSignal) await cacheSignal.cacheReady() - if (prospectiveRenderIsDynamic) { + // The handler may catch the prerender interruption and resolve + // normally, but dynamic tracking still records the request access. + const dynamicReason = getFirstDynamicReason(dynamicTracking) + + if (prospectiveRenderIsDynamic || dynamicReason) { // the route handler called an API which is always dynamic // there is no need to try again - const dynamicReason = getFirstDynamicReason(dynamicTracking) if (dynamicReason) { throw new DynamicServerError( `Route ${workStore.route} couldn't be rendered statically because it used \`${dynamicReason}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error` diff --git a/test/e2e/app-dir/cache-components/app/routes/caught-dynamic-url/route.ts b/test/e2e/app-dir/cache-components/app/routes/caught-dynamic-url/route.ts new file mode 100644 index 000000000000..c1b511d770f1 --- /dev/null +++ b/test/e2e/app-dir/cache-components/app/routes/caught-dynamic-url/route.ts @@ -0,0 +1,20 @@ +import type { NextRequest } from 'next/server' + +import { getSentinelValue } from '../../getSentinelValue' + +export async function GET(request: NextRequest) { + let search = '' + + try { + search = request.nextUrl.search + } catch { + console.log('caught dynamic URL access') + } + + return new Response( + JSON.stringify({ + value: getSentinelValue(), + search, + }) + ) +} diff --git a/test/e2e/app-dir/cache-components/cache-components.routes.test.ts b/test/e2e/app-dir/cache-components/cache-components.routes.test.ts index 13a3a70f8db6..2f8468a57e2e 100644 --- a/test/e2e/app-dir/cache-components/cache-components.routes.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.routes.test.ts @@ -50,6 +50,22 @@ describe('cache-components', () => { expect(json.search).toEqual('?foo=bar') }) + it('should stop the prospective render when a dynamic API error is caught', async () => { + if (!isNextDev) { + const prospectiveRenderLogs = next.cliOutput + .split('\n') + .filter((line) => line.includes('caught dynamic URL access')) + + expect(prospectiveRenderLogs).toHaveLength(1) + } + + const str = await next.render('/routes/caught-dynamic-url?foo=bar', {}) + const json = JSON.parse(str) + + expect(json.value).toEqual('at runtime') + expect(json.search).toEqual('?foo=bar') + }) + it('should prerender GET route handlers that have entirely cached io (fetches)', async () => { let str = await next.render('/routes/fetch-cached', {}) let json = JSON.parse(str) From cf41e173cabd36e00b3d03fc902ecf1c4701159d Mon Sep 17 00:00:00 2001 From: David Alexandru Ilie Date: Sun, 9 Aug 2026 01:01:31 +0200 Subject: [PATCH 2/3] Narrow caught request access detection --- .../next/src/server/route-modules/app-route/module.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index 0c7c9fa14a10..20985b1e727c 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -639,11 +639,17 @@ export class AppRouteRouteModule extends RouteModule< // The handler may catch the prerender interruption and resolve // normally, but dynamic tracking still records the request access. - const dynamicReason = getFirstDynamicReason(dynamicTracking) + const dynamicRequestAccess = dynamicTracking.dynamicAccesses.find( + ({ expression }) => + expression.startsWith('nextUrl.') || + expression.startsWith('request.') + )?.expression - if (prospectiveRenderIsDynamic || dynamicReason) { + if (prospectiveRenderIsDynamic || dynamicRequestAccess) { // the route handler called an API which is always dynamic // there is no need to try again + const dynamicReason = + dynamicRequestAccess ?? getFirstDynamicReason(dynamicTracking) if (dynamicReason) { throw new DynamicServerError( `Route ${workStore.route} couldn't be rendered statically because it used \`${dynamicReason}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error` From dce1ce2aaec8a442d18167e8e906303f9f96e6bc Mon Sep 17 00:00:00 2001 From: David Alexandru Ilie Date: Sun, 9 Aug 2026 01:09:57 +0200 Subject: [PATCH 3/3] Preserve the existing dynamic error path --- .../next/src/server/route-modules/app-route/module.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index 20985b1e727c..51d7bff29094 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -639,17 +639,16 @@ export class AppRouteRouteModule extends RouteModule< // The handler may catch the prerender interruption and resolve // normally, but dynamic tracking still records the request access. - const dynamicRequestAccess = dynamicTracking.dynamicAccesses.find( + const hasDynamicRequestAccess = dynamicTracking.dynamicAccesses.some( ({ expression }) => expression.startsWith('nextUrl.') || expression.startsWith('request.') - )?.expression + ) - if (prospectiveRenderIsDynamic || dynamicRequestAccess) { + if (prospectiveRenderIsDynamic || hasDynamicRequestAccess) { // the route handler called an API which is always dynamic // there is no need to try again - const dynamicReason = - dynamicRequestAccess ?? getFirstDynamicReason(dynamicTracking) + const dynamicReason = getFirstDynamicReason(dynamicTracking) if (dynamicReason) { throw new DynamicServerError( `Route ${workStore.route} couldn't be rendered statically because it used \`${dynamicReason}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`