JS SDK 的错误类与重试策略

先说结论

  • 官方 JS SDK 把错误分成连接/超时和 HTTP 错误两套;HTTP 错误统一继承自 APIError,并由 AuthenticationErrorRateLimitError 等子类细分。
  • 本文归纳的重试决策:连接失败、超时、429、5xx 适合退避重试;400、401、403、404、422 不应重试,应改输入、改权限或改代码。
  • SDK 已经内建有限次重试,默认重试 408、429、500–599,最多额外重试 2 次,并遵守 Retry-After。但业务层仍然需要幂等键、降级和最终失败处理。
  • 官方 RetryPolicy 接口暴露 9 个配置项,可按需覆盖;这些默认值是官方数据。

官方错误类怎么归类

先看 官方 JS SDK:APIError。这一页的定义很简短:

An unsuccessful HTTP response from the API.

同一页还列出了它的子类:

Extended by AuthenticationError BadRequestError InternalServerError NotFoundError PermissionDeniedError RateLimitError UnprocessableEntityError

也就是说,所有 HTTP 非 2xx 响应在 SDK 里都会落到 APIError 家族。按类名可以分成三组:

  • 认证/权限AuthenticationErrorPermissionDeniedError
  • 请求类BadRequestErrorUnprocessableEntityErrorNotFoundError
  • 服务端与限流InternalServerErrorRateLimitError

这是依据官方 APIError 页面的 Extended by 列表做的归类。注意,官方这页没有逐个写明每个子类对应哪个 HTTP 状态码;本文后面把 401/403/400/422/404 映射到错误类名,属于工程上常见的 HTTP 语义归类,不是官方页面原文。

连接类不在 APIError 子类列表中,但在 官方 RetryPolicy 接口里出现了。官方原文是:

Retry connection failures, including interrupted response bodies ( APIConnectionError ). Default: true.

以及:

Whether to retry APITimeoutError . Default: true.

所以 APIConnectionErrorAPITimeoutError 属于连接/超时失败,SDK 把它们作为独立于 HTTP 错误的两类来处理。

RateLimitError 的官方页面则直接写明:

HTTP 429: the rate limit was exceeded.

这是官方明确给出的状态码映射。来源见 官方 JS SDK:RateLimitError

重试决策表

下面的表是本文归纳,不是官方建议;分类依据是官方错误类名和 RetryPolicy 默认行为。

情况 官方相关证据 本文建议
连接失败/中断、超时 RetryPolicyAPIConnectionErrorAPITimeoutError 可配置重试 指数退避重试
429 限流 RateLimitError 页面写明 HTTP 429;RetryPolicy 默认重试 429 退避重试,并遵守 Retry-After
5xx 服务端错误 InternalServerErrorAPIError 子类;RetryPolicy 默认重试 500–599 退避重试,但多次失败要熔断
400、422 请求形状错误 BadRequestErrorUnprocessableEntityErrorAPIError 子类 不重试,改输入
401、403 凭据/权限问题 AuthenticationErrorPermissionDeniedErrorAPIError 子类 不重试,检查 key 与权限
404 未找到 NotFoundErrorAPIError 子类 不重试,改请求路径或资源

这里再次强调:官方 APIError 页面没有写下 401/403/400/422/404 这些具体状态码;上表中的 HTTP 状态码映射是本文根据类名和常用 HTTP 语义做的工程归类。

为什么内建重试还不够

官方 Models 页对限流和 SDK 重试有直接说明:

Rate limits: Measured in tokens per second and requests per minute. A request over either limit returns 429 Too Many Requests . Our client SDKs retry with backoff by default and honor the retry-after header when the response carries one.

同一页给出的当前 Jev 1.13 限流数字是官方数据:

250,000 tokens per second / 1,200 requests per minute

另外,官方 RetryPolicy 接口显示默认 maxRetries2,默认 httpStatuses408, 429, and 500–599。换句话说,SDK 已经把“暂时性错误”自动重试了一轮。

但 SDK 的重试是传输层的,它不知道你的业务是否安全重复执行。比如你已经成功收到模型结果、但客户端在落库前超时,SDK 重试一次,你的业务可能重复处理同一请求。因此业务层还需要做幂等键或去重表。这是工程上的常见做法,官方文档没有展开。

另一个原因是:SDK 重试耗尽后仍会抛错。比如连续 429、连续 5xx,最终需要业务层决定是快速失败、切换到规则判定,还是熔断。这个“最后一公里”不能依赖 SDK 默认行为。

一个可用的处理骨架

下面这段不是官方 SDK 方法名规范,而是基于官方 API 参考的 HTTP 端点和 SDK 的 APIError.fromResponse 构建的兜底/降级骨架。幂等键、超时降级函数都是工程做法,不是官方要求。

import { APIError, RateLimitError, InternalServerError } from "@typesafe-ai/sdk";

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const seenKeys = new Set<string>();

async function callSystemOneWithFallback(payload: unknown) {
  const key = crypto.randomUUID();
  // 本地幂等去重:实际应用中应返回缓存结果或等待已完成请求
  if (seenKeys.has(key)) return null;
  seenKeys.add(key);

  const url = "https://api.typesafe.ai/v1/systemone";
  let attempt = 0;
  const maxAttempts = 4;

  while (attempt < maxAttempts) {
    attempt += 1;
    const res = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.TYPESAFE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    const text = await res.text();
    const body = res.ok ? JSON.parse(text) : text;
    if (res.ok) return body;

    const err = APIError.fromResponse(res.status, body, res.headers);

    if (
      err instanceof RateLimitError ||
      err instanceof InternalServerError ||
      err.status === 408
    ) {
      const retryAfterHeader = res.headers.get("retry-after-ms");
      const retryAfter = Number(retryAfterHeader || 0);
      const backoff = Math.max(
        retryAfter || 500 * 2 ** (attempt - 1),
        5000
      );
      await sleep(Math.min(backoff, 60000) * (1 - Math.random() * 0.25));
      continue;
    }

    // 4xx 不重试:认证、权限、请求形状、未找到
    return ruleBasedFallback(payload, err.status);
  }

  throw new Error("max retries exceeded");
}

function ruleBasedFallback(payload: unknown, status: number) {
  // 降级到确定性规则判断,不调用模型
  return { status, fallback: true, payload };
}

代码里的 retry-after-ms header 名称来自 RetryPolicy 页面原文:

Honor Retry-After and retry-after-ms up to maxRetryAfterMs .

APIError.fromResponse 也是官方 APIError 页面列出的静态方法,用于根据 HTTP 状态码创建对应的错误子类。

RetryPolicy 配置项速查

以下默认值来自官方接口页,均为官方数据:

配置项 类型 默认值
apiConnectionError boolean true
apiTimeoutError boolean true
backoffInitialMs number 500
backoffJitter number 0.25
backoffMaxMs number 5000
httpStatuses ReadonlySet<number> 408, 429, 500–599
maxRetries number 2
maxRetryAfterMs number 60000
respectRetryAfter boolean true

官方页面还说明,这是“partial overrides inherit unset fields from the client or SDK defaults”,也就是部分覆盖时,未设置的字段会继承客户端或 SDK 默认值。

上线前清单

  • 不要随便把 maxRetries 设为 0,除非你完全接管重试逻辑。
  • 对写操作或带副作用的业务,必须接入幂等键或去重表。这是工程要求,不是官方要求。
  • 为 429 和 5xx 设置外部熔断,避免 SDK 重试把上游压垮。
  • 对 401、403、400、422、404 快速失败,结构化记录错误,避免重复消耗额度。
  • 超时降级路径要有规则判定兜底,不能只依赖模型结果。
  • 上线前模拟非幂等请求在超时、429、5xx 三种情况下的表现。

这次没核实的

  • 官方 APIError 页面没有逐个列出 AuthenticationErrorPermissionDeniedErrorBadRequestError 等各自对应的 HTTP 状态码;本文对 401/403/400/422/404 的映射是工程上常见 HTTP 语义的归类,不是官方页面原文。
  • 官方 Models 页提到“If you call the HTTP API directly, see Handling rate limits”,但本次给出的来源未包含该 Handling rate limits 子页面正文,其具体建议未能核实。
  • 官方 SDK 的评估调用方法名未出现在本次给出的 API reference 页面中;因此上面的处理骨架没有使用编造的 SDK 方法,只用 APIError.fromResponse 和 HTTP 端点。

参考来源

评论区

0 条评论

登录后可评论。

卷心菜 27 阅读