AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Instant-Navigation Validation Treats Deliberate Redirect() From Layouts As Validation Failure And Hides Real Diagnostics

In Next.js 16.3.5-16.4.0-canary.32, a redirect() inside a Suspense boundary in a layout triggers an instant navigation validation error on every warm request, although redirect is intentional control flow. This suppresses genuine blocking-prerender-dynamic findings on the same route.

mediumConfidence 85%Next.jsAffected V16.3.5Affected V16.4.0-Canary.32

Origin Analysis

Both onError and onBrowserBailout call sites in packages/next/src/server/app-render/app-render.tsx pass the thrown value directly to trackThrownErrorInNavigation without filtering router sentinels via isNextRouterError, so NEXT_REDIRECT is treated as a render error, causing validation to be inconclusive.
1. Create app/dashboard/layout.tsx that wraps children in a Suspense boundary and inside an async Shell component calls cookies() and redirect('/elsewhere') when a cookie is absent. 2. Create app/dashboard/page.tsx with a blocking Server Component using await connection() from 'next/server'. 3. Run npm run dev. 4. Request /dashboard twice without the cookie: curl -s -o /dev/null http://localhost:3000/dashboard (twice). 5. The second request logs 'Could not validate instant...' caused by NEXT_REDIRECT, while the expected blocking-prerender-dynamic warning for the page is missing.

Fixing Code Block

// In packages/next/src/server/app-render/app-render.tsx // Add import if not already present: import { isNextRouterError } from '../../client/components/is-next-router-error' // In both onError and onBrowserBailout callbacks, insert the following guard before trackThrownErrorInNavigation: if (isNextRouterError(err)) { throw err // Let Next.js handle router sentinels as control flow, not as a render error } trackThrownErrorInNavigation(err) // Example of updated onError (apply the same pattern to onBrowserBailout): onError: (err: unknown) => { if (isNextRouterError(err)) { throw err } trackThrownErrorInNavigation(err) }
The patch checks whether the thrown value is a router sentinel (NEXT_REDIRECT, NEXT_NOT_FOUND, etc.) using isNextRouterError. If it is, the sentinel is rethrown so the router can complete the navigation operation, and it is not logged as a validation error. This keeps instant navigation validation focused on actual render failures and avoids suppressing real blocking IO diagnostics.

Edge Case Audit

Re-throwing router sentinels could alter error propagation if the surrounding error boundary expects to swallow them. Test all router sentinel types (redirect, notFound, unauthorized, forbidden) in layouts, pages, and route handlers. Ensure this does not cause double handling in streaming or partial rendering. The fix should be limited to the two call sites; if regressions appear, revert to the original trackThrownErrorInNavigation call and instead filter inside getNavigationDisallowedDynamicReasons.

Ecosystem Topology