Post-auth navigation into a server-redirecting route hangs the App Router
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 1.8k
- Forks
- 472
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 189
Description
Summary
When Clerk performs its post-authentication client-side navigation into a route whose Server Component calls redirect(), the Next.js App Router never commits the navigation. The page hangs on the Rendering… indicator, parked on the intermediate route, while the server has already resolved the redirect and returned the destination.
Reproduces on the latest published release. A page reload clears it, so this is stuck client state rather than a server deadlock.
Reproduction
Minimal repro, derived from clerk/clerk-nextjs-app-quickstart with four routes added: https://github.com/manovotny/clerk-nextjs-post-auth-redirect-hang
pnpm install
# add a dev instance's keys to .env.local
pnpm dev
# open /repro
Click "1. Sign in → /setup" and complete sign-in. The button is a <SignInButton mode="modal" forceRedirectUrl="/setup">; /setup is a Server Component that calls await auth() then redirect('/landed').
Expected: land on /landed, which renders LANDED OK.
Actual: hangs on Rendering…, URL stays at /setup.
Environment
| Package | Version |
|---|---|
@clerk/nextjs |
7.7.4 (also reproduced on 7.7.1 and 7.5.18) |
@clerk/shared |
4.28.1 |
@clerk/react |
6.14.1 |
next |
16.2.10 |
react / react-dom |
19.2.5 |
Isolation
The repro ships controls. Results:
| Trigger | Destination | Result |
|---|---|---|
<Link> click, signed out |
/setup (auth() + redirect) |
lands |
<Link> click, signed out |
/plain (redirect only, no Clerk) |
lands |
<Link> click, signed in |
/setup |
lands |
| Post-auth push | /setup |
hangs on /setup |
| Post-auth push | /plain |
hangs on /plain |
The single variable is Clerk's post-auth push. Specifically ruled out:
- Not
auth()on the destination. The Clerk-free/plaincontrol hangs identically. - Not being signed in. The same route via
<Link>while signed in works. - Not the redirect itself.
/setupand/plainboth return307and resolve to/landed200on every other path. - Doesn't look like a Suspense/streaming problem. Adding
app/setup/loading.tsx(the redirecting segment, not a route-group root) didn't clear the hang for us.
Additional observations
- The server completes fully. During a hang the dev server logs the destination returning
200twice. - No console errors. Verified with devtools open for the whole flow.
- The UI renders the signed-out state (
<Show when="signed-out">branch) despite authentication having succeeded — suggestingsetActivenever finishes its post-navigation steps.
Server log captured during a hang:
POST /repro 200
└─ ƒ invalidateCacheAction() in 9ms
GET /setup 200
GET /landed 200
GET /landed 200
Suspected mechanism — hypothesis, not traced
I haven't instrumented this. Offering it as a starting point, with the reasoning, so it can be confirmed or discarded quickly.
packages/nextjs/src/app-router/client/useInternalNavFun.ts wraps router.push in startTransition() and resolves the queued navigation promises only when isPending flips back to false:
// useInternalNavFun.ts
useEffect(() => {
if (!isPending) {
flushPromises();
}
}, [isPending]);
The file already documents a closely related failure mode (lines 25–36), where useTransition's isPending can get permanently stuck at true when Next.js deduplicates or cancels a redundant router action — currently guarded only for the duplicate-destination case:
// Calling startTransition with a duplicate router.push()
// can permanently stick useTransition's isPending at `true` when Next.js
// deduplicates/cancels the redundant router action.
Hypothesis: pushing to a route that server-redirects is another way to reach that stuck state. The pushed destination (/setup) never becomes the committed route, since the router ends up resolving /landed instead, so isPending never settles the way flushPromises() expects. The navigation promise never resolves, setActive never completes, and window.__internal_onAfterSetActive → router.refresh() (set in app-router/client/ClerkProvider.tsx) never fires.
This would account for every observation: the park on the intermediate route, the successful server fetches, the signed-out UI, the silence in the console, loading.tsx having no effect, and <Link> clicks working (they never enter useInternalNavFun).
Cheap way to confirm or refute
While the page is hung, in the console:
window.__clerk_internal_navigations.push
If promisesBuffer.length > 0 and pendingDestination === '/setup', the navigation promise never flushed and the hypothesis holds. If the buffer is empty, the stall is downstream and this is the wrong tree.
Relevant files
packages/nextjs/src/app-router/client/useInternalNavFun.ts— transition wrapper and promise bufferpackages/nextjs/src/app-router/client/useAwaitablePush.ts/useAwaitableReplace.tspackages/nextjs/src/app-router/client/ClerkProvider.tsx—__internal_onBeforeSetActive/__internal_onAfterSetActivepackages/nextjs/src/app-router/server-actions.ts—invalidateCacheAction
Impact
This isn't an exotic configuration. clerk/clerk-nextjs-app-quickstart ships app/protected/page.tsx doing await auth() then redirect() with no loading.tsx, so the pattern is in the template we hand new users. Any post-auth landing route that redirects — onboarding gates, org selection, role-based routing — is exposed.
Source of the report
Originally surfaced by an external contributor in clerk/clerk-docs#3525, which proposed documenting a loading.tsx workaround. The symptom report was accurate; the proposed mechanism (ClerkProvider state clashing with a blocking transition) looks wrong, and the loading.tsx fix didn't resolve the hang in our repro.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the linked reproduction using pnpm install and pnpm dev, then inspect packages/nextjs/src/app-router/client/useInternalNavFun.ts and the awaitable navigation hooks. Trace the post-auth push through ClerkProvider.tsx and server-actions.ts, checking the navigation promise buffer while reproducing the hang. Done means post-auth navigation commits to the redirected destination and the existing relevant tests cover this route-redirect case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- authentication, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100