在行情接口的排障裡,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 重試永遠無效
權限問題不會因為時間流逝而改變。更麻煩的是,盲目重試會帶來三個副作用:
- 掩蓋故障:重試邏輯把明確的權限錯誤變成「偶發失敗」,延誤定位。
- 浪費額度:被拒絕的請求如果計入了速率計數,會擠壓正常請求的配額。
- 汙染監控:錯誤率指標被稀釋,告警閾值失去意義。
正確做法是:403 一律不重試,立即告警,並把 code 與 X-Request-Id 一起記錄下來。
排查流程
- 讀
code,確認屬於六類中的哪一類。 - 記錄
X-Request-Id,聯繫支持時提供。 - 如果是訂閱類(NO_SUBSCRIPTION / SUBSCRIPTION_EXPIRED):檢查賬號訂閱狀態與賬單。
- 如果是市場類(MARKET_NOT_ENABLED):確認該市場在當前套餐下的權限等級,以及你請求的是實時還是延遲接口。
- 如果是密鑰類(API_KEY_SCOPE_DENIED):核對密鑰範圍,檢查是否多環境混用。
- 如果是策略類(IP_DENIED / GEO_DENIED):確認實際出口 IP 與部署區域。
- 在客戶端把 403 與 429 分開處理:403 直接失敗,429 中的速率類才允許重試。
Python 實現:區分 403 與 429 的錯誤處理
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 鑑權指南。如果確認是套餐權限問題,可對照 套餐定價 查看各市場的實時與延遲權限,或直接查閱 加密貨幣行情 等具體市場頁面。