休市提醒日股将于 2026-11-03 休市(文化日),距今天还有 5 天。全部公告
文章

403 错误全解析:行情接口权限被拒的六种原因

403 不是 429,等多久都不会恢复。本文拆解行情接口 403 的六种成因——订阅状态、市场未开通、密钥范围、IP 与地区策略——并说明为什么重试永远无效,附区分 403 与 429 的 Python 错误处理实现。

在行情接口的排障里,403 和 429 经常被混为一谈,但它们的性质完全不同:429 是「用量超限」,等一等可能就恢复;403 是「权限不足」,等多久都不会恢复。

把 403 当成 429 处理,最典型的后果是写了一个自动重试逻辑,然后眼睁睁看着服务连续失败一整天。这篇文章把 403 的六种成因逐个拆开。

403 的六种成因

行情接口的 403 至少对应六种不同的权限问题,每一种的解法都不同:

错误码含义解法
NO_SUBSCRIPTION没有生效中的订阅订阅对应套餐
SUBSCRIPTION_EXPIRED订阅已过期续订或检查账单状态
MARKET_NOT_ENABLED当前套餐未开通该市场升级套餐或改请求已开通市场
API_KEY_SCOPE_DENIED密钥权限范围不含该接口用范围正确的密钥
IP_DENIED来源 IP 被策略拒绝调整 IP 白名单或改用服务端出口
GEO_DENIED来源地区被策略拒绝调整部署区域或联系支持

和 429 一样,判断依据是响应体里的 code 字段,而不是 HTTP 状态码本身。六个错误码都是 403,但处理路径完全不同。

NO_SUBSCRIPTION 与 SUBSCRIPTION_EXPIRED:订阅状态问题

这两个错误码说明账号层面没有可用的订阅。区别在于:前者是「从未订阅或已取消」,后者是「曾经有效但已过期」。

值得注意的是,免费套餐本身也是一种订阅。如果你在免费套餐下请求了套餐范围外的市场,得到的应该是 MARKET_NOT_ENABLED 而不是 NO_SUBSCRIPTION。所以看到 NO_SUBSCRIPTION 时,优先检查账号的订阅状态与账单,而不是请求参数。

MARKET_NOT_ENABLED:最常见的 403

这是实际排障中遇到最多的一类。它的含义很明确:你的套餐没有覆盖请求的市场。

不同套餐的市场权限差异很大,而且要注意「实时」与「延迟」是分开授权的。典型情况是:某个市场在你的套餐里是延迟数据,如果你按实时接口去请求,就会收到 MARKET_NOT_ENABLED。这种情况下升级套餐确实能解决,但先确认你真正需要的是实时还是延迟数据,往往能省下一笔开支。

排查方法很简单:先确认目标市场在当前套餐下的权限等级,再对照请求的接口类型。如果只是要延迟数据,用对应的接口即可。

API_KEY_SCOPE_DENIED:密钥范围问题

密钥可以按用途限定范围。当你用一把「只读行情」的密钥去访问别的接口,或者用一把限定给某个服务的密钥去访问另一个服务时,就会命中这个错误。

这类问题常见于两种场景:

  • 多环境密钥混用:开发环境的密钥被误部署到生产,而它没有生产所需的权限范围。
  • 密钥轮换未同步:新密钥创建时范围配置错了,但旧密钥已撤销。

建议按环境(开发 / 预发 / 生产)分别创建密钥,并遵循最小权限原则。密钥一旦泄漏应立即在控制台撤销并轮换,不要只改代码里的字符串。

IP_DENIED 与 GEO_DENIED:网络策略问题

这两类错误来自访问策略,而不是套餐权限。常见成因:

  • 出口 IP 变了:服务从固定 IP 的服务器迁移到弹性 IP 或容器环境,出口地址不再在白名单里。
  • 本地开发误伤:在办公室或家宽环境下用生产密钥调试,本机 IP 未在白名单。
  • 部署区域变更:服务迁移到新的云区域,触发了地区策略限制。

排查时先确认请求的实际出口 IP(注意容器、NAT、代理都可能改变它),再对照白名单与地区策略。

为什么对 403 重试永远无效

权限问题不会因为时间流逝而改变。更麻烦的是,盲目重试会带来三个副作用:

  1. 掩盖故障:重试逻辑把明确的权限错误变成「偶发失败」,延误定位。
  2. 浪费额度:被拒绝的请求如果计入了速率计数,会挤压正常请求的配额。
  3. 污染监控:错误率指标被稀释,告警阈值失去意义。

正确做法是:403 一律不重试,立即告警,并把 code 与 X-Request-Id 一起记录下来。

排查流程

  1. 读 code,确认属于六类中的哪一类。
  2. 记录 X-Request-Id,联系支持时提供。
  3. 如果是订阅类(NO_SUBSCRIPTION / SUBSCRIPTION_EXPIRED):检查账号订阅状态与账单。
  4. 如果是市场类(MARKET_NOT_ENABLED):确认该市场在当前套餐下的权限等级,以及你请求的是实时还是延迟接口。
  5. 如果是密钥类(API_KEY_SCOPE_DENIED):核对密钥范围,检查是否多环境混用。
  6. 如果是策略类(IP_DENIED / GEO_DENIED):确认实际出口 IP 与部署区域。
  7. 在客户端把 403 与 429 分开处理:403 直接失败,429 中的速率类才允许重试。

Python 实现:区分 403 与 429 的错误处理

Python
import httpx

API_KEY = "your_api_key"
BASE = "https://api.tiqex.io/api/v1/market"

# 403 全部不重试;只有速率超限类 429 才重试
FATAL_403 = {
    "NO_SUBSCRIPTION", "SUBSCRIPTION_EXPIRED", "MARKET_NOT_ENABLED",
    "API_KEY_SCOPE_DENIED", "IP_DENIED", "GEO_DENIED",
}
RETRYABLE_429 = {"RATE_LIMIT_EXCEEDED"}


class FatalAccessError(Exception):
    """权限类错误:重试无用,需要人工介入。"""


def get_quote(codes: str):
    resp = httpx.get(
        f"{BASE}/quote",
        params={"codes": codes},
        headers={"X-API-Key": API_KEY},
        timeout=10,
    )
    if resp.status_code == 200:
        return resp.json()

    body = resp.json() if resp.content else {}
    code = body.get("code")
    request_id = resp.headers.get("X-Request-Id")

    if resp.status_code == 403:
        hint = {
            "NO_SUBSCRIPTION": "账号无生效订阅,请检查订阅状态与账单",
            "SUBSCRIPTION_EXPIRED": "订阅已过期,请续订",
            "MARKET_NOT_ENABLED": "当前套餐未开通该市场,或请求了错误的实时/延迟接口",
            "API_KEY_SCOPE_DENIED": "密钥权限范围不含该接口,请核对密钥配置",
            "IP_DENIED": "来源 IP 不在白名单,请确认实际出口 IP",
            "GEO_DENIED": "来源地区被策略拒绝,请检查部署区域",
        }.get(code, "未知权限错误")

        # 直接抛出,绝不重试
        raise FatalAccessError(f"{code}: {hint} (requestId={request_id})")

    raise RuntimeError(f"请求失败 code={code} requestId={request_id}")


try:
    print(get_quote("BTCUSDT,AAPL"))
except FatalAccessError as e:
    print("需人工处理:", e)
    # 在这里上报告警,而不是进入重试队列

核心是让 403 走一条完全独立的异常路径——它不进重试队列,而是直接触发告警。这能避免权限故障被重试逻辑掩盖成「偶发问题」。

常见问题

403 和 429 怎么快速区分?

看响应体的 code 字段。如果错误在等待一段时间后自动消失,基本是 429;如果持续存在且与请求量无关,基本是 403。

免费套餐也会返回 403 吗?

会。免费套餐只覆盖部分市场的延迟数据,请求套餐范围外的市场会得到 MARKET_NOT_ENABLED。

为什么同一个密钥在本地能用、在服务器上不行?

大概率是 IP_DENIED。密钥本身有效,但服务器的出口 IP 不在白名单里。注意容器平台、NAT 网关和代理都会改变实际出口 IP。

升级套餐能解决所有 403 吗?

不能。只有 MARKET_NOT_ENABLED 与订阅类错误可能通过升级解决。API_KEY_SCOPE_DENIED、IP_DENIED、GEO_DENIED 属于配置与策略问题,升级套餐无效。

被 403 拒绝的请求会消耗额度吗?

业务额度通常不会因此消耗,但请求仍会触发速率计数。更重要的是,大量 403 会让错误率指标失真,建议在监控里把 403 单独分组。

下一步

把 403 从重试逻辑里彻底剥离,是行情接入里性价比最高的一次改动。错误码的完整定义见 错误与限流文档;密钥的创建、分环境管理与轮换规则见 鉴权与密钥文档 与 REST 鉴权指南。如果确认是套餐权限问题,可对照 套餐定价 查看各市场的实时与延迟权限,或直接查阅 加密货币行情 等具体市场页面。