调用 API 时,401 Unauthorized、403 Forbidden 和 429 Too Many Requests 是最常见的三类错误。

它们都属于 4xx 客户端错误,但解决方法完全不同:

  • 401:认证失败——服务器无法确认“你是谁”
  • 403:访问被拒绝——服务器理解请求,但不允许执行
  • 429:请求过多——调用速度或用量超过限制

如果不区分错误类型就直接重试,不仅无法解决问题,还可能制造重试风暴,甚至导致接口被进一步限制。

本文将通过排障清单、curl 命令和 Node.js 代码,说明遇到这三类错误时应该如何处理。

一张表看懂 401、403 和 429

状态码 核心含义 常见原因 是否应该立即重试
401 Unauthorized 缺少有效认证凭证 API Key 错误、Token 过期、认证头格式错误 不应该原样重试
403 Forbidden 服务器拒绝处理请求 权限不足、IP 或地域限制、资源策略、WAF 拦截 通常不应该
429 Too Many Requests 请求超过速率或配额限制 并发过高、QPS 超限、额度耗尽 等待后重试

需要注意,虽然 401 的英文名称是 Unauthorized,但在实际排障时,更适合把它理解为“未通过认证”;403 才通常与授权或访问策略有关。

一、401 Unauthorized:先检查认证凭证

HTTP 401:API 凭证缺失、无效或已经过期

HTTP 401 Unauthorized 表示请求缺少目标资源所需的有效认证凭证。标准的 401 响应通常会通过 WWW-Authenticate 响应头告诉客户端服务器期望使用的认证方案。

常见原因

  1. 没有发送 Authorization 请求头
  2. API Key 或 Access Token 填写错误
  3. Token 已过期或被撤销
  4. 认证方案错误,例如遗漏 Bearer
  5. 环境变量没有正确加载
  6. 使用了错误环境的密钥
  7. 请求被代理或网关转发时,认证头丢失
  8. 客户端与服务器时间偏差过大,导致签名失效

常见错误

接口要求 Bearer Token:

Authorization: Bearer YOUR_ACCESS_TOKEN

但客户端发送成了:

Authorization: YOUR_ACCESS_TOKEN

或者错误地写成:

Authentication: Bearer YOUR_ACCESS_TOKEN

这两种情况都可能返回 401。

使用 curl 排查

不要一开始只查看响应体。加上 -i,同时检查状态码和响应头:

curl -i "https://api.example.com/v1/profile" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Accept: application/json"

重点检查:

  • 是否确实发送了 Authorization
  • 认证方案是否正确
  • Token 前后是否存在空格或换行
  • WWW-Authenticate 返回了什么
  • 响应体中是否有 invalid_token、expired_token 等错误信息

如需查看更详细的请求过程,可以使用:

curl -v "https://api.example.com/v1/profile" \
  -H "Authorization: Bearer $API_TOKEN"

注意:调试日志可能包含 Token。不要把完整日志直接发到公开 Issue、群聊或工单中。

401 应该重试吗?

不要使用同一份无效凭证反复重试。

推荐流程:

  1. 检查请求是否携带认证信息
  2. 检查 Token 是否过期、撤销或复制错误
  3. 如果支持 Refresh Token,刷新一次访问令牌
  4. 使用新 Token 重试一次
  5. 如果仍然返回 401,停止重试并记录错误

如果使用固定 API Key,则应检查密钥状态或更换有效密钥,而不是循环重试。

二、403 Forbidden:认证可能成功,但请求仍被拒绝

HTTP 403:请求因权限或访问策略被拒绝

403 Forbidden 表示服务器已经理解请求,但拒绝处理。对于典型的权限错误,即使重新认证,也不会自动解决问题;如果不修改请求或权限配置,重复请求通常仍会失败。

常见原因

  • 当前账号或密钥缺少所需权限
  • Token 的 Scope 不足
  • 访问了不属于当前项目或租户的资源
  • 来源 IP 不在白名单内
  • 请求来源地域受到限制
  • WAF、防火墙或反滥用系统拦截
  • 账号、项目或资源被停用
  • 接口仅向特定套餐或角色开放
  • 签名有效,但签名中的资源范围不匹配
  • 浏览器场景下受到 CSRF、Origin 或 Referer 策略限制

401 与 403 的区别

可以用两句话快速判断:

  • 401:服务器无法确认你是谁
  • 403:服务器知道或理解你的身份与请求,但不允许你这么做

例如,一个普通成员携带有效 Token 调用“删除组织”接口:

curl -i -X DELETE \
  "https://api.example.com/v1/organizations/org_123" \
  -H "Authorization: Bearer $API_TOKEN"

如果 Token 有效,但账号不是组织管理员,服务器可能返回:

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "insufficient_permissions",
  "message": "Administrator role is required."
}

403 排障顺序

1. 阅读响应体

优先寻找错误代码、缺少的角色或权限、所需 Scope、被拒绝的资源 ID,以及 IP、地域或安全策略提示。

2. 检查 Token Scope

例如,读取数据可能只需要 data:read,但创建数据可能需要 data:write。Token 有效不代表拥有全部权限。

3. 检查资源归属

确认项目 ID、组织 ID、租户 ID、用户 ID、区域或环境与当前凭证属于同一个账号或授权范围。

4. 检查网络和安全策略

如果代码在本地运行正常,部署后却返回 403,应重点检查服务器出口 IP、IP 白名单、代理配置、CDN、WAF、请求来源地域和相关请求头。

不要只凭状态码下结论

现实中的 API 实现不一定完全一致。某些平台在触发限流时可能返回 403,而不是 429。例如,GitHub REST API 可能在超过速率限制时返回 403 或 429,并通过 x-ratelimit-remaining、x-ratelimit-reset 或 retry-after 等响应头说明限制状态。

因此,遇到 403 时还要综合检查:

  • 响应体中的业务错误码
  • Retry-After
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
  • 平台官方文档

403 应该重试吗?

对于权限不足、IP 限制或资源策略导致的 403,原样重试没有意义。

正确做法是修改权限或 Token Scope、使用正确账号或项目、调整白名单与安全策略、修正资源 ID,并确认服务或套餐是否支持该接口。只有在平台明确说明该 403 实际代表临时限流时,才应按限流规则等待后重试。

三、429 Too Many Requests:先减速,再重试

HTTP 429:API 请求受到限流,应等待并退避重试

429 Too Many Requests 表示客户端在一段时间内发送了过多请求,也就是触发了速率限制。服务器可以通过 Retry-After 告诉客户端应等待多长时间再重试。

常见原因

  • 短时间内请求次数过多
  • 并发任务数量过高
  • 多个服务共享同一 API Key
  • 重试代码没有等待机制
  • 定时任务在同一时刻集中执行
  • 请求没有缓存,重复获取相同数据
  • 单个账号、IP 或项目的配额已耗尽
  • 套餐用量或余额限制

首先检查响应头

curl -i "https://api.example.com/v1/resources" \
  -H "Authorization: Bearer $API_TOKEN"

重点关注:

Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710000000

不同平台的响应头名称可能不同,应以平台文档为准。

正确处理 Retry-After

Retry-After 常见的两种格式是秒数或 HTTP 日期:

Retry-After: 30
Retry-After: Wed, 21 Oct 2026 07:28:00 GMT

客户端最好同时兼容这两种格式。

为什么不能立即重试?

如果请求被限流后立即重试,新请求仍会被限流;多个客户端还可能同时重试,形成“惊群”。更稳妥的方法是:

  1. 优先遵守 Retry-After
  2. 没有明确等待时间时,使用指数退避
  3. 在等待时间中加入随机抖动
  4. 设置最大重试次数
  5. 达到上限后返回明确错误

Node.js:安全处理 429

下面的示例使用 Node.js 内置的 fetch,优先解析 Retry-After;如果服务器没有提供该响应头,则使用带随机抖动的指数退避。

const sleep = (ms) =>
  new Promise((resolve) => setTimeout(resolve, ms));

function parseRetryAfter(value) {
  if (!value) return null;

  const seconds = Number(value);
  if (Number.isFinite(seconds) && seconds >= 0) {
    return seconds * 1000;
  }

  const timestamp = Date.parse(value);
  if (!Number.isNaN(timestamp)) {
    return Math.max(0, timestamp - Date.now());
  }

  return null;
}

async function fetchWithRetry(url, options = {}, config = {}) {
  const {
    maxRetries = 3,
    baseDelayMs = 1000,
    maxDelayMs = 30_000,
  } = config;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    if (attempt === maxRetries) {
      throw new Error(
        `请求持续受到限流,已达到最大重试次数:${maxRetries}`
      );
    }

    const retryAfterMs = parseRetryAfter(
      response.headers.get("retry-after")
    );

    const backoffCap = Math.min(
      maxDelayMs,
      baseDelayMs * 2 ** attempt
    );

    const jitterMs = Math.floor(Math.random() * backoffCap);
    const delayMs = retryAfterMs ?? jitterMs;

    console.warn(
      `收到 429,第 ${attempt + 1} 次等待 ${delayMs}ms 后重试`
    );

    await sleep(delayMs);
  }

  throw new Error("请求失败");
}

调用示例:

const response = await fetchWithRetry(
  "https://api.example.com/v1/resources",
  {
    headers: {
      Authorization: `Bearer ${process.env.API_TOKEN}`,
      Accept: "application/json",
    },
  },
  {
    maxRetries: 3,
    baseDelayMs: 1000,
    maxDelayMs: 30_000,
  }
);

if (!response.ok) {
  const errorBody = await response.text();
  throw new Error(`API 请求失败:${response.status} ${errorBody}`);
}

const data = await response.json();
console.log(data);

POST 请求重试时,还要考虑幂等性

并非所有请求都适合自动重试。GET、HEAD 等读取请求通常更容易安全重试。对于创建订单、扣款、发送消息等 POST 请求,客户端可能已经成功提交,但因为网络中断没有收到响应。此时直接重试,可能造成重复操作。

如果平台支持,应为写操作设置幂等键:

curl -i -X POST "https://api.example.com/v1/orders" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-20260331-0001" \
  -d '{
    "product_id": "prod_123",
    "quantity": 1
  }'

幂等键的请求头名称和有效期由具体平台决定,应参考相应 API 文档。

生产环境排障清单

收到 401

  • 请求中是否包含认证头
  • Bearer 等认证方案是否正确
  • Token 是否过期、撤销或复制错误
  • 是否使用了正确环境的密钥
  • 代理或网关是否删除了认证头
  • 客户端时间是否准确
  • 是否可以安全地刷新一次 Token

收到 403

  • Token 是否具有所需 Scope
  • 当前账号是否具备所需角色
  • 资源是否属于当前项目或租户
  • 出口 IP 是否在白名单中
  • 是否受到地域、WAF 或安全策略限制
  • 响应体是否明确提示权限或限流
  • 响应头中是否存在限流信息

收到 429

  • 是否读取了 Retry-After
  • 是否检查限流相关响应头
  • 是否使用指数退避和随机抖动
  • 是否设置最大重试次数
  • 是否限制并发
  • 是否缓存重复查询结果
  • 是否可以合并或批量发送请求
  • 是否达到套餐配额或账户用量上限

建议记录哪些诊断信息?

建议在日志中记录请求时间、API 主机名和路径、HTTP 方法、状态码、业务错误代码、request-id 或 trace-id、Retry-After、限流剩余额度与重置时间、重试次数和实际等待时间。

但不要记录完整 API Key、完整 Access Token、Refresh Token、用户密码或请求体中的敏感个人信息。

最后的判断口诀

401:先修凭证,不要拿旧 Token 重试。
403:先修权限或策略,不要原样重复请求。
429:尊重等待时间,退避、降速、限并发。

状态码只是排障入口,不是全部答案。可靠的处理方式,是同时结合 HTTP 状态码、响应体中的业务错误代码、相关响应头、平台文档、请求 ID 和服务端日志,把“接口报错”变成一套可观察、可恢复、可维护的工程机制。

参考资料