A protected page calling `redirect()` if the user is missing. Author placed it after data fetching and inside conditional JSX. Next.js threw on every render — `redirect` interrupts the render cycle.
The vulnerable diff
// app/dashboard/page.tsx
import { redirect } from "next/navigation";
export default async function Dashboard() {
const user = await getCurrentUser();
const data = await fetchDashboardData(user?.id); // crashes if no user
return (
<div>
{!user && redirect("/login")} {/* BUG: in render */}
<Stats data={data} />
</div>
);
}What is wrong
Next.js's `redirect()` works by throwing a special error that the framework catches and converts to a 307. Called inside render — particularly after `await`s have started — the throw interrupts the rendering process, breaks Suspense boundaries, sometimes triggers an infinite render loop if not handled. The correct pattern is to call `redirect()` early, before any other work, at the top of the server component.
The attack
Repro: visit /dashboard while logged out. Either you see an error boundary or the page reloads in a loop.
# Logs:
Error: NEXT_REDIRECT — caught by error boundary instead of framework.
The redirect did not fire; the boundary rendered "Something went wrong".Sometimes the redirect does happen but only after the error boundary triggers a re-render, leading to flashing UI.
Mesrai's review comment
mesraipilot · Bot · reviewed 1 min ago
[mesrai] [code-review] [Language] [Next-Redirect] [medium]
`redirect()` works by throwing. Call it at the top of the server
component, before any awaited work or JSX. Two problems with the
current placement:
1. fetchDashboardData runs even when there is no user — wastes
the call, might 500 because user.id is undefined.
2. The redirect inside the JSX expression executes during render,
which interrupts the render tree and can break Suspense.
Fix shape:
export default async function Dashboard() {
const user = await getCurrentUser();
if (!user) redirect("/login");
// user is non-null past this point
const data = await fetchDashboardData(user.id);
return <Stats data={data} />;
}
If you need authorization (not just authentication), do that early too:
if (!user) redirect("/login");
if (!user.canViewDashboard) redirect("/no-access");The fix
// app/dashboard/page.tsx — fixed
import { redirect } from "next/navigation";
export default async function Dashboard() {
const user = await getCurrentUser();
if (!user) redirect("/login");
// user is non-null after this guard
const data = await fetchDashboardData(user.id);
return <Stats data={data} />;
}Redirect is the first guard after auth fetch. Data fetch only happens for logged-in users. The user is non-null after the guard so TypeScript can narrow correctly. No conditional inside JSX, no awaits between the auth check and the redirect.
Why human review missed it
Next.js's redirect-as-throw pattern is non-obvious. The error message when it misfires is unclear. The bug shows up only in specific code paths (logged-out users on a logged-in page). Mesrai catches `redirect()` calls inside JSX or after non-trivial awaits, and recommends moving them to the top of the server component.
Related rules + further reading
Mesrai rule pack: language/next-redirect-placement — flags redirect() inside JSX or after data fetches.
Next.js docs: redirect() in Server Components.
Common pattern as teams migrate from useEffect-based redirects to server-side.
Takeaway
Redirect early. Before awaits, before JSX. Throws clean. Mesrai catches the misplaced pattern.