错误处理
@tripo3d/auth 把 Kratos 多种错误形态(HTTP 4xx、表单校验、字段级提示、自定义 422、网络异常)归一化成一个 TripoAuthError。业务侧只需要 catch 一种异常,根据 error.name 分流即可。
概述
SDK 内部定义的 TripoAuthError 类(未对外导出):
// 仅供理解,业务侧不能 import
class TripoAuthError extends Error {
name: string; // 8 类来源,见下文
message: string; // 给用户看的描述
constructor({ message, name }: { message: string; name: string }) {
super(message);
this.name = name;
this.message = message;
}
}由于该类未导出,业务侧无法做 instanceof TripoAuthError 判断;改用 instanceof Error 配合 error.name 字段分流即可。
TripoAuthError 没有 code 字段——所有分类信息都在 name 上。需要细分时再读 message。
唯一例外是 OIDC 的 1001 continue-flow:它不是 TripoAuthError,而是一个普通对象 { code: '1001', data: { id } }。详见下文「特殊错误码」。
name 分类全表
| error.name | 触发条件 | 说明 |
|---|---|---|
Flow | `response.ui.messages` 或 `ui.nodes[*].messages` 含 `type === 「error」` | Kratos 表单校验或业务规则错误,如密码弱、邮箱已存在、验证码错 |
Response | `response.error = { code, message }` 字段存在 | Kratos HTTP 层错误,4xx/5xx 携带 code+message |
Unknown | fetch 异常,error.response 不是 Response 实例 | 网络错误、CORS、DNS 等未预期形态 |
OIDC | OIDC 弹窗回传 error 消息或被浏览器拦截 | 第三方授权失败、第三方报错、popup blocker 命中 |
CheckIdentifier | `/idp-ext/auth/check-identifier` 接口失败 | 识别 email/phone 是否已注册的预检接口异常 |
SendVerificationCode | `/idp-ext/auth/send-verification-code` 接口失败 | 422 continue-flow 业务后端发码失败 |
VerifyEmailCode | `/idp-ext/auth/verify-email-code` 接口失败 | 422 continue-flow 业务后端验码失败 |
ContinueFlow | `getContinueFlow` 拿不到 ui 字段 | continue-flow 步骤里取 Kratos action URL 失败 |
LoginFlow | `getLoginFlowMessages` / `getLoginFlowOutcome` 拿不到 ui 字段 | 反查指定 login flow 状态失败 |
Flow 错误深入
来源:Kratos 在响应里返回 200,但响应体的 ui 部分包含 type: 'error' 的消息。SDK 的 handleFlow 会扫两层:
- 全局错误
data.ui.messages— 整个表单层面的提示,例如"密码错误"、"账号被锁" - 字段错误
data.ui.nodes[*].messages— 某个字段的校验提示,例如 password 节点上的"密码长度至少 8 位"
收集到的所有错误消息会按顺序拼成一个数组,SDK 抛出第一条作为 error.message(代码注释明确"暂不处理所有错误")。
try {
await loginFlow.password({ email, password: '123' });
}
catch (err) {
if (err instanceof Error && err.name === 'Flow') {
// err.message 例如:「The provided credentials are invalid, check for spelling mistakes...」
toast.error(err.message);
}
}如果需要拿全量消息(比如把每个字段的错误展示在对应输入框旁),需要在创建 flow 时保留 flowId,事后调 getLoginFlowMessages:
// 1. 创建 flow 时记录 id
const loginFlow = await auth.createLoginFlow();
// SDK 没直接暴露 flowId,目前需要从内部 state 或外层管理
// 推荐做法:登录失败后用 getLoginFlowOutcome 反查
try {
await loginFlow.password(...);
}
catch (err) {
// 由于 SDK 在错误时也会更新内部 state.flow 到最新值,
// flow id 通过响应链路是可获得的,但当前公开 API 不直接返回。
// 实践中:把 createLoginFlow 用一层包装,自己保留 flow id。
}getLoginFlowMessages 返回 UiText[](Ory SDK 类型),每条包含 id / type / text / context,可按需展示。
Response 错误深入
来源:Kratos 返回 4xx/5xx,响应体形如 { error: { code: 401, message: 'unauthenticated' } }。SDK 直接拿 data.error.message 当 error.message。
常见 code:
| error.code(message 内含) | 含义 | 说明 |
|---|---|---|
401 | `unauthenticated` | Cookie 失效或未登录,业务方应清空本地 token 重新登录 |
403 | `forbidden` | 没有权限操作此资源,例如非管理员调管理 API |
410 | `flow expired` | Flow 超时(默认 1 小时),需要重新创建 |
429 | `rate limit exceeded` | 触发频率限制,提示用户稍后再试 |
500 | `internal server error` | Kratos 服务异常,应上报埋点 + 提示稍后重试 |
注意 error.message 是字符串,业务侧需要从中匹配关键词或自己维护映射表。code 不在结构化字段里,只能从 message 里提取。
catch (err) {
if (err instanceof Error && err.name === 'Response') {
if (err.message.includes('rate limit')) {
toast.error('操作过于频繁,请稍后再试');
}
else if (err.message.includes('unauthenticated')) {
router.push('/login');
}
else {
toast.error(err.message);
}
}
}特殊错误码:code: '1001'
OIDC 弹窗触发 continue-flow(第三方未提供邮箱)或账号合并(同邮箱已存在密码账号)时,主窗口的 oidc() 会以普通对象 reject:
{
code: '1001',
data: {
id: 'flow-id-here', // 后续用于 getContinueFlow / createMergeFlow
}
}它不是 Error 实例(普通 plain object),所以 instanceof Error 会是 false。catch 时要先按对象形状判断:
catch (err: any) {
if (err?.code === '1001') {
return enterContinueFlow(err.data.id); // 见 OIDC 登录页
}
if (err instanceof Error) {
handleByName(err);
}
}另一种非 Error 实例的 reject:用户取消 OIDC 时会 reject { handled: true, type: 'cancelled' } 普通对象,业务侧通常静默处理。
统一 catch 策略
推荐的统一异常处理模板:
async function safeAuth<T>(fn: () => Promise<T>) {
try {
return await fn();
}
catch (err: any) {
// OIDC 用户取消,静默
if (err?.handled && err?.type === 'cancelled') {
return;
}
// OIDC continue-flow 走专属流程
if (err?.code === '1001') {
await handleContinueFlow(err.data.id);
return;
}
if (err instanceof Error) {
switch (err.name) {
case 'Flow':
// 表单层面错误,直接给用户看
toast.error(err.message);
break;
case 'Response':
// HTTP 层错误,可能需要跳登录或重试
if (err.message.includes('unauthenticated')) {
router.push('/login');
}
else {
toast.error(err.message);
}
break;
case 'OIDC':
toast.error(err.message);
break;
case 'CheckIdentifier':
case 'SendVerificationCode':
case 'VerifyEmailCode':
case 'ContinueFlow':
case 'LoginFlow':
// 业务接口错误,toast 即可
toast.error(err.message);
break;
case 'Unknown':
// 网络/未预期错误,上报埋点 + 兜底提示
reportError(err);
toast.error('网络异常,请稍后重试');
break;
default:
reportError(err);
toast.error(err.message);
}
return;
}
// 非 Error 实例(代码 bug 或第三方库异常)
reportError(err);
throw err;
}
}checkIdentifier 错误
checkIdentifier(value) 用业务后端的 /idp-ext/auth/check-identifier 预检 email/phone 是否已注册,返回 { flowType: 'login' | 'registration' }。失败抛 TripoAuthError(name: 'CheckIdentifier'):
try {
const { flowType } = await auth.checkIdentifier('user@example.com');
if (flowType === 'login') {
showPasswordInput();
}
else {
showRegistrationForm();
}
}
catch (err) {
if (err instanceof Error && err.name === 'CheckIdentifier') {
// 接口本身故障,降级方案:跳过预检,让用户直接试登录
showPasswordInput();
}
}接口签名异常和"用户不存在"是两个概念——后者是 success=true 且 flowType='registration',不会抛错。
错误码翻译
Kratos 的错误消息分两个层级,SDK 都会扫:
{
"ui": {
"messages": [
{ "id": 4000010, "type": "error", "text": "The provided credentials are invalid" }
],
"nodes": [
{
"attributes": { "name": "password" },
"messages": [
{ "id": 4000005, "type": "error", "text": "Password is too weak" }
]
}
]
}
}SDK handleFlow 会先扫 ui.messages 再扫 ui.nodes[*].messages,把所有 type === 'error' 的 text 收集成数组,抛出第一条。如果想做 i18n 或字段级展示,可以用 getLoginFlowMessages 拿原始 UiText[],按 id(Kratos 的稳定错误码)做映射:
const i18n = {
4000010: () => t('login.invalidCredentials'),
4000005: () => t('password.tooWeak'),
// ...
};
const messages = await auth.getLoginFlowMessages(flowId);
for (const m of messages) {
if (m.type === 'error') {
const localized = i18n[m.id]?.() ?? m.text;
toast.error(localized);
}
}完整 Kratos 错误 id 列表见 Ory 官方文档。
生产环境最佳实践
Flow/OIDC用 toast — 这两类是用户操作可恢复的错误(密码错、第三方拒绝),不阻塞页面,toast 即可Response区分 401 / 429 / 5xx:- 401 → 清空本地 token,跳登录页
- 429 → toast「请稍后再试」+ 禁用按钮 N 秒
- 5xx → toast + 上报埋点
Unknown必须上报 — 这是兜底分支,通常意味着代码或环境异常,埋点能帮助定位CheckIdentifier失败做降级 — 跳过预检直接进登录/注册流程,不要因为预检接口挂了就阻塞用户- 422 链路要有重试 —
sendVerificationCode/verifyEmailCode是业务后端,失败时让用户重新点「发送验证码」按钮即可,无需重启 OIDC 流程 - 不要 catch 后吞掉 —
default分支务必reportError或throw,否则线上问题不可见
调试技巧
- 开发环境把
allowedOrigins设为[],可以接受任何来源的 postMessage,排查弹窗通信问题 - Kratos 的
flow id写在 URL?flow=...上,可直接GET /self-service/login/flows?id=...查看完整 ui 结构 tokenized()返回 undefined 一般是 session cookie 过期 / 跨域 cookie 没带,优先检查浏览器 DevTools 的 Cookie 标签- 内部抛出的
TripoAuthError继承Error,stack trace 完整保留,失败时直接console.error(err)即可看到调用链
下一步
- 架构总览 — 错误归一化在整体架构中的位置
- OIDC 登录 —
1001continue-flow 的完整处理 - API Reference — 自动生成的完整签名