curl 能过、Python 报 403:排查 API 端点最容易误判的三类假象

简介: 如果你的 Python 脚本调 API 返回 403,而完全相同的请求用 curl 是 200,先别怀疑 key。大概率是 urllib / requests 的默认 User-Agent 被 CDN 的 WAF 拦了——它返回的 403 和「鉴权失败」的 403 长得几乎一样,但根本不是一回事。

先给结论:如果你的 Python 脚本调 API 返回 403,而完全相同的请求用 curl 是 200,先别怀疑 key。大概率是 urllib / requests 的默认 User-Agent 被 CDN 的 WAF 拦了——它返回的 403 和「鉴权失败」的 403 长得几乎一样,但根本不是一回事。

判别只要一步:看 403 的响应体。

响应体长什么样 真正的原因
纯文本 error code: 1010 WAF 拦截,跟你的 key 无关
JSON {"error":{"type":"authentication_error",...}} 鉴权真的失败了
JSON {"error":{"type":"permission_error",...}} key 有效但没这个权限

下面是三条容易走错的排查路径,以及最后怎么定位的。文中回包为示意示例,非真实抓包。命令都能直接复制运行。

示例环境:WSL2 (Ubuntu) + Python 3.12,端点用的是「你的端点地址」这类 Anthropic 兼容网关,前面有 CDN/WAF。任何前置 CDN 的端点都会复现同样的现象。作者所在的 Code2AI 属于这类服务,文末有披露。


一、现象:同端点、同 key、同 payload,两个客户端两个结果

我在跑一个自检脚本,四项检查全部返回 403。脚本本身逻辑很简单,就是往 /v1/messages 发几个最小请求。

① 假模型名探测    ⚠️  无法确认
   HTTP 403,响应不是 Anthropic 的 `{
   "type":"error","error":{
   ...}}` 结构
② 上游响应头      ⚠️  无法确认
③ prompt caching  ⚠️  无法确认
   HTTP 403
④ usage 字段结构  ⛔ 请求失败

四项全灭,看起来像是这个端点整个不可用,或者 key 废了。

但换 curl 发同一个请求(模型用 Anthropic 的 claude-sonnet-4-6):

curl -sS -X POST "$ANTHROPIC_BASE_URL/v1/messages" \
  -H "content-type: application/json" \
  -H "x-api-key: $ANTHROPIC_AUTH_TOKEN" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"claude-sonnet-4-6","max_tokens":8,
       "messages":[{"role":"user","content":"hi"}]}' \
  -w "\n[HTTP %{http_code}]\n"
{
   "id": "msg_01d6f8...", "type": "message", "role": "assistant",
 "content": [{
   "type": "text", "text": "Hi!"}],
 "usage": {
   "input_tokens": 2, "output_tokens": 8, ...}}
[HTTP 200]

200。 同一个 key,同一个 payload,同一台机器,差别只在客户端。


二、三条容易走错的排查路径

排到这一步很容易往三个方向猜,这三个方向都是错的。写下来省得你重复走。

猜测 怎么验证 实测结果
key 无效或过期 换 curl 打同一个请求 ❌ 200,key 是好的
鉴权头形式不对 x-api-key 与 Authorization: Bearer 各打一次 ❌ 两种都 200
端点挂了 / 路径不对 不带任何凭证打一次,看它怎么回 ❌ 端点活得好好的

2.1 别急着换 key

最直觉的反应是「key 是不是废了」。验证成本很低,换个客户端打一次就知道:

curl -sS -o /dev/null -w "%{http_code}\n" -X POST "$ANTHROPIC_BASE_URL/v1/messages" \
  -H "content-type: application/json" -H "x-api-key: $ANTHROPIC_AUTH_TOKEN" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"claude-sonnet-4-6","max_tokens":8,"messages":[{"role":"user","content":"hi"}]}'

返回 200 就说明 key 没问题,问题在你的客户端。这一条能砍掉一大半排查方向。

2.2 两种鉴权头都试一次

Anthropic 协议用 x-api-key,但很多兼容网关同时接受 Authorization: Bearer。如果只试了一种,容易误判成「这个端点不认我的鉴权方式」。

两种都返回 200(示意):

# 形式一
-H "x-api-key: $ANTHROPIC_AUTH_TOKEN"
# 形式二
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN"

顺带一提,这两种都支持是兼容网关的常见做法,不代表任何异常。

2.3 不带凭证打一次——这一步信息量最大

这是我认为最被低估的一条排查动作:故意不带 key 发一个请求,看端点怎么拒绝你。

curl -sS -X POST "$ANTHROPIC_BASE_URL/v1/messages" \
  -H 'content-type: application/json' -d '{}'
{
   "error":{
   "type":"invalid_request_error",
  "message":"API key required. Use 'x-api-key' or 'Authorization: Bearer' header",
  "request_id":"c2a-990d2533"}}

这一个请求同时告诉你三件事:

  1. 端点是活的,能正常处理请求
  2. 它的错误响应是标准 JSON 结构,带 type 和 request_id
  3. 它接受哪些鉴权头——直接写在报错里了

第 2 点是关键:既然这个端点拒绝请求时会返回结构化 JSON,那我脚本收到的那个非 JSON 的 403,就一定不是这个端点发出来的。

是中间有人替它回了。


三、真正的原因:默认 User-Agent

urllib 不设置 User-Agent 时,默认发的是 Python-urllib/3.12。Cloudflare 一类的 WAF 会直接拒掉这类特征明显的客户端签名。

它返回的东西长这样:

HTTP 403
error code: 1010

纯文本,没有 JSON 结构。 Cloudflare 的 1010 是「基于浏览器签名拒绝访问」。

这个 403 的迷惑性在于:

  • 状态码和「鉴权失败」完全一样
  • 大部分客户端代码只看 status_code,不看 body
  • 于是它被当成「key 不对」或「没权限」,排查方向从第一步就偏了

四、控制变量确认:只改 UA

要坐实这个判断,把其他变量全固定住,只改 User-Agent 跑一次对照:

import os, json, urllib.request, urllib.error

BASE = os.environ["ANTHROPIC_BASE_URL"]
BODY = json.dumps({
   "model": "claude-sonnet-4-6", "max_tokens": 8,  # Anthropic 的模型
                   "messages": [{
   "role": "user", "content": "hi"}]}).encode()

def go(ua):
    h = {
   "content-type": "application/json",
         "x-api-key": os.environ["ANTHROPIC_AUTH_TOKEN"],
         "anthropic-version": "2023-06-01"}
    if ua:
        h["user-agent"] = ua
    req = urllib.request.Request(BASE + "/v1/messages", data=BODY,
                                 headers=h, method="POST")
    try:
        r = urllib.request.urlopen(req, timeout=30)
        return f"HTTP {r.status}"
    except urllib.error.HTTPError as e:
        return f"HTTP {e.code}  body: {e.read()[:60].decode('utf-8','ignore')}"

print("默认 UA:", go(None))
print("普通 UA:", go("my-tool/1.0"))

对照输出(示意):

默认 UA: HTTP 403  body: error code: 1010
普通 UA: HTTP 200

一行 header 的差别。 其余全部相同。

修复就是给你的客户端一个正常的 UA。建议报上工具自己的身份,而不是伪装成浏览器——目的是可被识别,不是绕过:

USER_AGENT = "my-tool/1.0 (+https://github.com/yourname/yourrepo)"

requests 库默认 UA 是 python-requests/2.x,同样会被拦,处理方式一样。


五、同一家族的另外两类假象

「表现像 A、实际是 B」的问题不止这一个。下面两类同样高频,同样会把人带偏。

5.1 代理环境变量:看起来像「端点不通」

本机配过 http_proxy / https_proxy,而那个代理地址已经不可用了。于是每个请求都要先去撞一次超时,最后报连接失败——看起来和「端点挂了」一模一样。

诊断:

env | grep -i proxy

如果有输出,先摘掉再测:

env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY \
  curl -sS -o /dev/null -w "%{http_code}\n" "$ANTHROPIC_BASE_URL/v1/messages" -X POST -d '{}'

自检脚本里最好直接把这几个变量摘掉——你要测的是端点,不是本机的网络配置:

for k in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY",
          "all_proxy", "ALL_PROXY"):
    os.environ.pop(k, None)

这类问题按「本机 → DNS → 端点」三层逐项排除即可,每层都可以用上面的命令验证。

5.2 base_url 多写了一段:404 而不是 403

配 ANTHROPIC_BASE_URL 时把 /v1/messages 也写进去了:

# ❌ 错误
export ANTHROPIC_BASE_URL="https://example.com/v1/messages"

客户端会自己补 /v1/messages,实际请求路径变成 /v1/messages/v1/messages,然后吃一个莫名其妙的 404。写到域名为止就行:

# ✅ 正确
export ANTHROPIC_BASE_URL="https://example.com"

判别方法是直接看实际请求的完整 URL。


六、一个通用原则:排查外部端点,先建两个对照组

上面三类假象的共同点是——故障现象出现的位置,和故障原因所在的位置,不是同一个地方。报错在你的脚本里,原因在 CDN、在本机环境变量、在配置字符串里。

想快速定位,动手改代码之前先建两个对照组:

对照组 怎么做 能排除什么
换客户端 同一个请求用 curl 再打一次 区分「端点问题」和「客户端问题」
去掉凭证 故意不带 key 打一次 确认端点活着、看清它的错误结构长什么样

两条都跑完,绝大多数「403 / 404 / 超时」类问题的方向就定了。剩下的才值得去读代码。

另外一项更难自己想到的是 MTU 黑洞:小请求正常、一发长内容就卡死,因为路径 MTU 发现依赖的 ICMP 被中间设备丢了,大包静默进黑洞——表现是「卡住」而不是「报错」。


FAQ

Q:为什么 curl 能过,Python 不能?

curl 默认会发 User-Agent: curl/8.x,WAF 通常放行;urllib 默认发 Python-urllib/3.x,属于被拦截的特征。差别只在这一个 header。

Q:换 requests 库会不会好一点?

不会。requests 默认 UA 是 python-requests/2.x,同样在拦截名单里。显式设置 UA 才是解法。

Q:自己加 User-Agent 算不算绕过风控?

看你加成什么。报上工具自己的名字和仓库地址是标准做法,HTTP 规范里 UA 本来就是用于标识客户端的;把 UA 伪装成 Chrome 浏览器则是另一回事。这两者的区别是「让我可以被识别」和「让我看起来像别人」。

Q:怎么快速确认是 WAF 拦截还是端点本身拒绝?

看 403 的响应体。WAF 返回的通常是纯文本或一整页 HTML;API 端点返回的是带 type 和 request_id 的结构化 JSON。另外看响应头有没有 CF-RAY、Server: cloudflare 这类 CDN 特征。

Q:状态码就一定能说明问题吗?

不能,这正是本文的主题。403 可能来自 WAF、可能来自鉴权、可能来自权限;404 可能是路径拼错、可能是模型名不存在。状态码只是入口,响应体才是证据。


小结

现象 先查什么 而不是
脚本 403、curl 200 客户端 User-Agent 换 key
403 body 是纯文本 CDN / WAF 鉴权配置
所有请求超时 `env \ grep -i proxy` 端点可用性
配置看着都对却 404 base_url 是否多写了 /v1/messages 重装客户端

排查外部端点的时候,先固定变量,再改代码。换个客户端打一次、去掉凭证打一次,这两个动作加起来不到一分钟,能省掉一下午。

利益披露:作者在做 Code2AI 的 API 兼容网关类服务,属于上文提到的前置 CDN 的 Anthropic 兼容端点这一类。

相关文章
|
8天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
7475 13
|
7天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1571 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
7天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
1059 9
|
3天前
|
人工智能 JavaScript 芯片
DeepSeek 官方偷偷上传 Harness 桌面端安装包,我已经用上了。。附最新下载地址
DeepSeek Harness 官方的桌面端安装包被网友扒出来了,2 分钟讲明白如何使用,体验如何,适合作为 AI 编程工具么?附最新 Windows 和 Mac 双端的下载地址
1290 1
|
20天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3610 10
|
15天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
1637 1
|
4天前
|
编解码 缓存 PyTorch
16G 显卡能跑 Qwen-Image 2.1 吗?
9月20日,阿里Qwen开源Qwen-Image-2.1:7B DiT图像模型+8B文本编码器+VAE,单模型支持文生图与图像编辑,原生输出2K PNG(含Alpha通道),支持10张参考图。在自建Qwen-Image-Bench达60.28分(开源模型第一),GenAI Showdown文生图排名7/15。16G显存可跑1024×1024(需INT8量化+ComfyUI优化),但2K需24G以上。注意其Qwen Research License限非商业用途。
534 1
|
5天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)

热门文章

最新文章