错误处理
@tripo3d/auth v2 把所有错误收敛到一个类——TripoAuthError,并固定 6 个 name 分类。业务侧按 error.name 分流即可,几乎所有路径都能写出可预测的 catch 模板。
TripoAuthError
import { TripoAuthError } from '@tripo3d/auth';
class TripoAuthError extends Error {
override readonly name: TripoAuthErrorName;
readonly code?: string; // ory 错误体里抽出来的 id / code,便于精细分流
override readonly cause?: unknown; // 原始 ory error / response / TypeError 等
override readonly message: string;
}
type TripoAuthErrorName
= | 'FLOW'
| 'OIDC'
| 'VALIDATION'
| 'NETWORK'
| 'INTERNAL'
| 'PRIVILEGED_SESSION_EXPIRED';字段语义:
name— 6 类枚举(见下表);业务侧主要按这个分流code— ory 错误体里抽出来的id/error.id/error.code,例如session_inactive、self_service_flow_expired;可空cause— 包了{ context, error }或原始 error;调试期可看,生产期不直接展示给用户message— 优先取 oryui.messages→ui.nodes[*].messages→error.message→ fallback;可以直接 toast 给用户
6 类 name
| name | 典型来源 | 说明 |
|---|---|---|
FLOW | ory flow 调用失败:HTTP 非 2xx、ui.messages 含 error、flow 已过期、CSRF 不匹配、验证码错 | auth.login.* / auth.reset.* / auth.oidc.loadEdgeFlow 几乎所有「业务正常路径走不通」的错;message 直接 toast 给用户 |
OIDC | startOidc 拿到的 redirect_browser_to 缺失或非法;OIDC 完成页 URL 字段不合法(mode/provider 缺失) | 业务逻辑通常需要降级到登录页(无法继续 OIDC);toast 后跳走 |
VALIDATION | 业务侧 API 用法不对:submitCode 前没 sendCode;reset.verifyCode 前没 sendCode、reset.submitNewPassword 前没 verifyCode;buildCompleteUrl 收到非法 completeUrl / 跨站 returnTo;loadEdgeFlow flowId 空 | 开发期 bug;UI 状态应卡住这些路径。生产环境兜底跳走 + 上报 |
NETWORK | fetch 抛 TypeError;ory 调用拿到 FetchError / NetworkError / AbortError;HTTP 5xx 服务端故障 | 统一文案「服务异常,请稍后重试」;建议加重试按钮,不要静默吞掉 |
INTERNAL | SDK 自检异常:TripoAuth 构造缺 basePath;flow ui.nodes 取不到 csrf_token | 上游配置或 SDK 使用错误;上报 + 不要直接展示原文(开发期信息) |
PRIVILEGED_SESSION_EXPIRED | auth.settings.updatePassword / auth.reset.submitNewPassword 时当前 session 不够 privileged(多半是 verifyCode 与改密之间间隔超过 ory 的 privileged_session_max_age,默认 15 min) | toast 提示「验证已过期,请重新发送验证码」+ UI 切回验证码一步(settings.sendVerifyCode / reset.sendCode);不要上报 |
「未登录 / 会话已失效」语义不走错误流:fetchSessionToken 与 createDefaultFetchToken 都把它吞掉返回 undefined——业务侧 await auth.token.get() 拿到 undefined 即视作未登录,无需 try/catch。
HTTP 5xx 归类为 NETWORK
服务端 5xx 故障在语义上是基础设施问题(不是业务流程走不通),v2 把它归类为 NETWORK 而非 FLOW。业务方可借此走「服务异常 / 稍后重试」分支,而不是把 ory 文案透传给用户。
推荐 catch 模板
import { TripoAuthError } from '@tripo3d/auth';
function handleAuthError(error: unknown) {
if (!(error instanceof TripoAuthError)) {
console.error(error);
toast.error('网络异常,请稍后重试');
captureException(error);
return;
}
switch (error.name) {
case 'FLOW':
case 'OIDC':
// 业务正常路径走不通:直接 toast 把 ory 文案展示
toast.error(error.message);
return;
case 'NETWORK':
toast.error('网络异常,请稍后重试');
// 不上报 NETWORK:高频噪音
return;
case 'VALIDATION':
// 开发期 bug;生产兜底跳登录页
console.warn('[auth] validation failed:', error.message);
captureException(error);
router.push('/login');
return;
case 'INTERNAL':
console.error('[auth] internal:', error.message);
captureException(error);
toast.error('登录服务暂不可用,请刷新页面');
return;
case 'PRIVILEGED_SESSION_EXPIRED':
// 改密场景(settings.updatePassword / reset.submitNewPassword):verifyCode 与改密之间停太久,session 不再 privileged
// 业务侧应把 UI 切回验证码一步让用户重新发码;不要上报(不是 bug)
toast.warning('验证已过期,请重新发送验证码');
switchToSendVerifyCodeStep();
return;
}
}
// 未登录场景:直接走「token.get() === undefined」分流,无需进 catch
const token = await auth.token.get();
if (!token) {
router.push('/login');
}各场景错误来源细化
验证码登录
| 抛错位置 | name | 典型 message / code |
|---|---|---|
sendCode 内部 /idp-ext/auth/check-identifier fetch 失败 | NETWORK | 'check-identifier request failed' |
sendCode 内部 check-identifier 返回非 2xx / success: false | FLOW | body.error 或 'check-identifier failed (HTTP ${status})' |
sendCode / submitCode 内 ory flow 拒收(验证码错 / flow 过期 / 频控) | FLOW | 透传 ory ui.messages |
submitCode 在没先 sendCode 的情况下被调 | VALIDATION | 'submitCode requires a prior sendCode call in the same session' |
| flow ui.nodes 取不到 csrf | INTERNAL | 'csrf_token node not found in flow ui' |
OIDC
| 抛错位置 | name | 典型 message |
|---|---|---|
startOidc 拿到的 redirect_browser_to 缺失 | OIDC | 'OIDC start did not yield a redirect_browser_to URL' |
startOidc 拿到的 redirect_browser_to 不是合法 URL | OIDC | 'OIDC redirect_browser_to is not a valid URL: ...' |
buildCompleteUrl 收到跨站 returnTo | VALIDATION | '[buildCompleteUrl] returnTo must be same-site tripo3d.ai: ...' |
buildCompleteUrl 收到非法 completeUrl | VALIDATION | '[buildCompleteUrl] invalid completeUrl: ...'(必须是绝对 URL) |
readCompleteContext URL 解析失败 | OIDC | '[readCompleteContext] invalid URL: ...' |
readCompleteContext mode/provider 缺失或非法 | OIDC | '[readCompleteContext] invalid or missing mode: ...' 等 |
loadEdgeFlow flowId 空 | VALIDATION | 'loadEdgeFlow requires a non-empty flowId' |
loadEdgeFlow probe 失败(非 merge 语义) | FLOW | 'edge probe failed with status ${status}' |
edge continue submit form 时 document 不可用 | OIDC | 'edge continue submit requires a browser environment' |
密码重置
| 抛错位置 | name | 典型 message |
|---|---|---|
verifyCode 在没先 sendCode 的情况下被调 | VALIDATION | 'reset.verifyCode requires a prior reset.sendCode call in the same session' |
submitNewPassword 在没先 verifyCode 的情况下被调 | VALIDATION | 'reset.submitNewPassword requires a prior reset.verifyCode call in the same session' |
verifyCode 验证码错 / flow 过期 | FLOW | 透传 ory ui.messages |
submitNewPassword 新密码弱 / 新旧相同 | FLOW | 透传 ory ui.messages(提权保留,可原地改密码重试) |
submitNewPassword 提权过期(ory 回 session_refresh_required) | PRIVILEGED_SESSION_EXPIRED | 引导回验证码步重发码 |
Token
auth.token.* 的失败行为:
peek()— 不抛错get()/refresh()— 透传fetchToken抛的错(自定义 fetchToken 可能抛任意类型,默认实现透传 oryTripoAuthError);保留旧 cacheset(jwt)— 不抛错;非法 JWT 静默忽略clear()— 不抛错
TripoAuth 构造
| 抛错位置 | name | 典型 message |
|---|---|---|
| 构造缺 basePath | INTERNAL | 'TripoAuth requires a non-empty basePath' |
fetchSessionToken 缺 basePath | INTERNAL | 'fetchSessionToken requires a non-empty basePath' |
onLogin handler 抛错的隔离
SignalApi 内部 invoke 把每个 handler 包在独立的 try/catch + Promise.resolve().catch 里:
private invoke(handler, signal) {
try {
Promise.resolve(handler(signal)).catch(err => this.logHandlerError(err));
} catch (err) {
this.logHandlerError(err);
}
}后果:
- 同步抛错 / 异步 reject 都不会泄漏出来
- 一个 handler 抛错不会影响别的 handler 派发
- 错误默认走
console.warn,但可以通过构造时传入的onError钩子统一监控(code: 'onlogin_handler_failed',详见下一节) - 业务侧自己想精细上报 → 在 handler 里包 try/catch + 显式
captureException
auth.signal.onLogin(async ({ signal }) => {
try {
// SDK 已在派发前做了 token.refresh 兜底;保留 refresh 作为容错(内部并发去重,不会重复打后端)
await auth.token.refresh();
await fetchCurrentUser();
} catch (err) {
captureException(err);
// SDK 同时会把这个错误抛给 onError 钩子(onlogin_handler_failed)
}
});onError 非阻塞错误观察钩子
TripoAuthOptions.onError?: (err: TripoAuthError) => void 是构造时传入的非阻塞错误观察回调。SDK 在以下「静默运营错误」发生时调用它——这些错误不抛出、不打断业务流,仅供监控:
| 场景 | err.code |
|---|---|
| JWT 续期网络失败 | 'token_refresh_failed' |
续期拿到非法 JWT(无 exp / decode 失败) | 'token_invalid_jwt' |
业务 onLogin handler 抛错 | 'onlogin_handler_failed' |
不传 onError 时默认回落 console.warn。
const auth = new TripoAuth({
basePath: 'https://auth.tripo3d.ai',
onError: (err) => {
// 统一上报这些非阻塞运营错误
captureException(err, { tags: { authErrorCode: err.code } });
},
});注意:onError 只覆盖上述静默运营错误;业务正常路径抛出的 TripoAuthError(FLOW / OIDC / VALIDATION 等)仍然通过方法的 await/catch 抛给业务方,不走 onError。
showLogin 异常兜底
showLogin(auth, options) 返回 void,自身不抛错:
- 宿主忘了挂
<DialogOpener />→dialog.show返回空串 → dialog 不弹出,无副作用 - 用户点 ✕ / ESC → dialog 自己关闭,无
outcome概念 - dialog 内部的 LoginPanel 抛错时(如
submitCode拒收)→ 由 dialog 自己渲染 toast 反馈,不会冒到调用栈 - 登录完成(含 mobile OIDC redirect 整页跳走)的唯一消费路径是
auth.signal.onLogin(handler)
因此 showLogin 不需要 try/catch、也没有返回值要分流;业务方只要先注册 onLogin handler 再唤起登录即可。
logout 错误路径
auth.logout() 内部三步:
ory createBrowserLogoutFlow()拿到logout_urlfetcher(logoutUrl, { method: 'GET' })让 ory 失效 sessionclearSignal()+ 外层token.clear()
错误处理:
| 路径 | 行为 |
|---|---|
步骤 1 抛 401 / session_inactive / 「unauthorized」 | 视作已登出,跳过步骤 2 直接走步骤 3 清理(不抛错) |
| 步骤 1 抛其他错 | rethrow,不动本地状态(外层 token.clear() 也不会跑) |
| 步骤 2 fetch 抛错 | rethrow,不动本地状态 |
| 全部成功 | 走步骤 3,清 signal cookie + token cache |
业务侧典型处理:
async function onLogout() {
try {
await auth.logout();
router.push('/');
} catch (err) {
if (err instanceof TripoAuthError && err.name === 'NETWORK') {
toast.error('网络异常,请稍后重试');
} else {
handleAuthError(err);
}
}
}生产环境最佳实践
FLOW/OIDC直接 toasterror.message:ory 已经把 UI 文案准备好了NETWORK给统一文案 + 重试按钮:不要把 ory 错误透传给用户,体验差VALIDATION必须上报:通常是 SDK 用法 bugINTERNAL必须上报 + 兜底:上游配置错;至少让用户能逃出当前页- 不要在 try/catch 里依赖
error.code:当前 SDK 透传 oryerror.id/error.code,ory 升级时可能变;安全做法是文本匹配error.message或类型匹配error.name onLoginhandler 抛错可通过onError钩子监控:默认console.warn;传onError即可统一上报(code: 'onlogin_handler_failed'),无需在每个 handler 里手动包 try/catch- OIDC redirect 模式没有同步 catch 机会:所有失败语义只可能体现在
OidcCompletePage的兜底 toast 或事后onLogin没触发
调试技巧
- 主窗
onLogin没触发:先看document.cookie里有没有__tripo_login_signal__;没有 → 写入端有问题;有 → 读取端有问题(polling 是否启动?constructor 时已经被消费了?) startOidc抛VALIDATION:99% 是returnTo跨站;改成tripo3d.ai同站submitCode抛FLOW但 message 模糊:看error.cause,里面的{ context, error }有原始 ory response- token.get() 返回 undefined 但你确认浏览器 cookie 在:ory
toSession失败;用await auth.token.refresh()直接调一次看抛什么;网络层错查error.cause.error拿 fetch error 原文
下一步
- 架构总览
- OIDC 登录 —
OIDC与FLOW错误的主要发源地 - 登录信号 (cookie) —
onLoginhandler 的错误隔离机制 - Token 管理 —
auth.token.*各方法的失败行为细节 - Playground