Skip to content

错误处理

@tripo3d/auth 把 Kratos 多种错误形态(HTTP 4xx、表单校验、字段级提示、自定义 422、网络异常)归一化成一个 TripoAuthError。业务侧只需要 catch 一种异常,根据 error.name 分流即可。

概述

SDK 内部定义的 TripoAuthError 类(未对外导出):

ts
// 仅供理解,业务侧不能 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
Unknownfetch 异常,error.response 不是 Response 实例网络错误、CORS、DNS 等未预期形态
OIDCOIDC 弹窗回传 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(代码注释明确"暂不处理所有错误")。

ts
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:

ts
// 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.messageerror.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 里提取。

ts
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:

ts
{
  code: '1001',
  data: {
    id: 'flow-id-here', // 后续用于 getContinueFlow / createMergeFlow
  }
}

不是 Error 实例(普通 plain object),所以 instanceof Error 会是 false。catch 时要先按对象形状判断:

ts
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 策略

推荐的统一异常处理模板:

ts
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'):

ts
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 都会扫:

json
{
  "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 的稳定错误码)做映射:

ts
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 官方文档。

生产环境最佳实践

  1. Flow / OIDC 用 toast — 这两类是用户操作可恢复的错误(密码错、第三方拒绝),不阻塞页面,toast 即可
  2. Response 区分 401 / 429 / 5xx:
    • 401 → 清空本地 token,跳登录页
    • 429 → toast「请稍后再试」+ 禁用按钮 N 秒
    • 5xx → toast + 上报埋点
  3. Unknown 必须上报 — 这是兜底分支,通常意味着代码或环境异常,埋点能帮助定位
  4. CheckIdentifier 失败做降级 — 跳过预检直接进登录/注册流程,不要因为预检接口挂了就阻塞用户
  5. 422 链路要有重试sendVerificationCode / verifyEmailCode 是业务后端,失败时让用户重新点「发送验证码」按钮即可,无需重启 OIDC 流程
  6. 不要 catch 后吞掉default 分支务必 reportErrorthrow,否则线上问题不可见

调试技巧

  • 开发环境把 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) 即可看到调用链

下一步

基于 MIT 协议发布(内部使用)