<OAuthConsent /> never mounts (blank page) when reached via <SignIn/>/<SignUp/> redirect_url in Next.js App Router: mountOAuthConsent silently no-ops during setActive()'s transitive state
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 52/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
- 技术栈
- nextjs, react, typescript
调研方向
从 Clerk.setActive()、mountOAuthConsent 和 ClerkHostRenderer 开始,然后通过 useAwaitablePush 和 useInternalNavFun 跟踪所提到的 app-router/client/ClerkProvider.js 路径。复现文档中所述的从登录到同意的导航,并验证 OAuthConsent 会在传递状态结束后挂载,且不需要重新加载,同时确保已登出时的行为仍然正确。
由索引模型根据 Issue 内容生成。
描述
Preliminary Checks
- I have reviewed the documentation: https://clerk.com/docs
- I have searched for existing issues: https://github.com/clerk/javascript/issues
- I have not already reached out to Clerk support via email or Discord (if you have, no need to open an issue here)
- This issue is not a question, general help request, or anything other than a bug report directly related to Clerk. Please ask questions in our Discord community: https://clerk.com/discord.
Reproduction
The reproduction is the documented custom consent page itself (https://clerk.com/docs/nextjs/reference/components/authentication/oauth-consent) plus the default <SignIn /> page, in a Next.js App Router app. Full code is inline below; I can push it to a public repo if that helps triage.
// app/oauth-consent/page.tsx — verbatim from the docs
import { OAuthConsent, Show } from '@clerk/nextjs'
export const metadata = { referrer: 'strict-origin-when-cross-origin' }
export default function OAuthConsentPage() {
return (
<Show when="signed-in">
<OAuthConsent />
</Show>
)
}
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'
export default function Page() { return <SignIn /> }
Clerk Dashboard → Paths → "OAuth consent" points at /oauth-consent, and an OAuth application with the consent screen enabled.
Publishable key
pk_live_Y2xlcmsuYWN0aW9uYm9vay5hcHAk
Description
Steps to reproduce:
- Sign out (or open a fresh browser profile).
- Start an OAuth authorization from any client. Clerk's authorization endpoint sends the signed-out user to the app's sign-in page with
redirect_urlpointing at the custom consent page, e.g.https://<app>/sign-in?redirect_url=https%3A%2F%2F<app>%2Foauth-consent%3Fclient_id%3D...%26scope%3D...%26redirect_uri%3D...%26state%3D...%26code_challenge%3D.... (Opening that URL directly reproduces it just as well.) - Sign in — or sign up — on that page with any strategy (Google via
/sign-in/sso-callback, password, password + MFA all behave the same). <SignIn />completes and Clerk navigates to/oauth-consent?....
Expected behavior:
The consent screen renders.
Actual behavior:
In a production instance the page stays blank forever, with no console error (session recordings of the affected users show zero console errors). Reloading the exact same URL renders the consent screen immediately. Users who already have a session (Clerk redirects them straight to the consent page with a full page load) are not affected, which makes this look like a "new user" bug in product analytics: in our data every consent page view that immediately followed a sign-in/sign-up in the same tab got no "Allow" click (8/8, users sat on the blank page for 10–254 s, then reloaded or gave up), while every full-load arrival rendered and was allowed within 2–4 s (6/6). All sessions were Chrome/Edge on desktop.
Root cause (from reading the shipped bundles):
This is an interaction between four pieces that are all inside the SDK:
-
Clerk.setActive()(clerk-js) enters its transitive state before navigating —this.session = undefined; this.organization = undefined; this.user = undefined+ emit — thenawait this.navigate(redirectUrl), and only restores the accessors (#setAccessors) after that promise resolves. -
In
@clerk/nextjs(App Router)navigate()for a same-origin URL is the injectedrouterPush(app-router/client/ClerkProvider.js→useAwaitablePush→useInternalNavFun), i.e. a client-siderouter.pushinsidestartTransition, and the promise only resolves in auseEffectonceisPendingflips back to false — that is, after the new route has committed. So during the commit of/oauth-consent,clerk.user === undefined. -
@clerk/react'sClerkHostRenderercallsmount()exactly once incomponentDidMount;componentDidUpdateonly forwardsupdateProps. Nothing re-mounts whenuserlater becomes available. -
Clerk.mountOAuthConsent(added in #8335, clerk-js ≥ 6.7.3) starts with:if (!this.user) { if (this.#instanceType === 'development') throw new ClerkRuntimeError(warnings.cannotRenderOAuthConsentComponentWhenUserDoesNotExist, ...); return; // production: silent no-op, never retried }Verified in the bundle served from the CDN today (
@clerk/clerk-js@6.31.0,dist/clerk.browser.js).
The docs example makes it worse, not better: in a server component import { Show } from '@clerk/nextjs' resolves through the #components → react-server condition to app-router/server/controlComponents.js, whose Show decides with server-side auth() and puts <OAuthConsent /> straight into the RSC payload — there is no client-side isLoaded gate at all, so the component mounts at commit time, inside the transitive window, and mountOAuthConsent bails.
Net effect: for any router-integrated SDK, the documented sign-in → consent hand-off is a soft navigation that mounts <OAuthConsent /> while clerk.user is undefined, and production swallows it. Full-page loads (already-signed-in users, reloads, cross-origin client/touch redirects) initialize clerk-js from cookies before any mount, which is why they work.
Workaround we shipped: render <OAuthConsent /> from a 'use client' component gated on useAuth():
'use client'
import { OAuthConsent, useAuth } from '@clerk/nextjs'
export function OAuthConsentCard() {
const { isLoaded, userId } = useAuth()
if (!isLoaded || !userId) return <Spinner /> // isLoaded is false during the transitive state
return <OAuthConsent fallback={<Spinner />} />
}
useAuth() reports isLoaded: false while sessionId/userId are undefined, so the mount is deferred until #setAccessors runs.
Suggested fixes (any one of them is enough):
mountOAuthConsentshould treatuser === undefined(transitive) differently fromuser === null(signed out) — e.g. queue the node the waypremountOAuthConsentNodesdoes and mount once the session is restored — instead of returning permanently.- Or have
ClerkHostRenderer/withClerkre-attempt the mount when the user becomes available. - Or restore the accessors before resolving the router navigation in
setActive(). - At minimum, log a warning in production instead of a silent
return— this failed for months without a single error anywhere. - The docs example for the custom consent page should gate on the client-side
isLoaded(or use<ClerkLoaded>) rather than the serverShow.
Environment
System:
OS: macOS 26.3
CPU: (10) arm64 Apple M5
Binaries:
Node: 25.8.1
npm: 11.11.0
pnpm: 10.24.0
Browsers:
Chrome: 152.0.7977.66
Safari: 26.3
npmPackages:
@clerk/nextjs: ^7.8.2 => 7.8.2
@clerk/react: 6.14.7 (via @clerk/nextjs)
@clerk/shared: 4.30.1
@clerk/ui: ^1.30.8 => 1.30.8
@clerk/localizations: ^4.15.7
next: ^16.3.4
react: ^19.2.4
react-dom: ^19.2.4
Runtime (loaded from the Clerk CDN via the frontend API host):
@clerk/clerk-js: 6.31.0 (npm/@clerk/clerk-js@6 → 6.31.0)
@clerk/ui: npm/@clerk/ui@1
- 主要语言
- TypeScript
- 星标
- 1.8k
- 派生
- 472
- 平均合并
- 2 天 15 小时
- 30 天内合并 PR
- 193
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
clerk/javascript 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 86/100
clerk/javascript#9852 ·
-
难度 2/5 1-3 小时 新手友好度 74/100
clerk/javascript#9611 · 1 条评论 ·
-
难度 2/5 1-3 小时 新手友好度 86/100
clerk/javascript#9573 ·
-
难度 4/5 3-5 天 新手友好度 64/100
clerk/javascript#9775 · 1 条评论 ·
-
难度 4/5 3-5 天 新手友好度 48/100
clerk/javascript#9770 · 3 条评论 ·
相似的 Issue
-
comp/desktop P3 type/bug
难度 1/5 1 小时以内 新手友好度 92/100
NousResearch/hermes-agent#118866 ·
-
Browser Waiting for: Product Owner
难度 2/5 1-3 小时 新手友好度 85/100
getsentry/sentry-javascript#24577 · 1 条评论 ·
-
难度 2/5 1-3 小时 新手友好度 78/100
agilepathway/label-checker#640 ·
-
Plugin stuck at "loading" on DSH 0.1.6-alpha.2 — turnTail list slot registration missing options.id 未关闭
难度 2/5 1-3 小时 新手友好度 88/100
-
难度 2/5 1-3 小时 新手友好度 68/100
chrisparsons83/flexspotff#153 ·