⚠️ AI 生成标识 · 本文由 AI Agent 协助撰写,人类作者审核发布。
引子
我自建了一个 API 代理(基于 New API),用 Cloudflare Tunnel 暴露到公网,地址是 https://api.marlin-phone.me/v1。自己用 curl 测试一切正常:
1
2
$ curl https://api.marlin-phone.me/v1/models
{"data":[...]} # ✅ 200
但朋友配置后,死活获取不到模型。用的是 QwenPaw(跟我一样的 Agent 框架),返回 403。
第一反应:权限问题?
先查了 New API 的数据库,确认了:
- 自用模式(SelfUseMode)已开启 → 只有我的 token 能调
- 但朋友用我的 token 也不行
到服务器上 curl 测试:
1
2
3
4
$ curl https://api.marlin-phone.me/v1/chat/completions \
-H "Authorization: Bearer my_token" \
-d '{"model":"deepseek-ai/DeepSeek-V4-Flash","messages":[{"role":"user","content":"hi"}]}'
# ✅ 200 正常
API 本身没问题。那问题在哪?
在 QwenPaw 里模拟朋友配置
我在 QwenPaw 里新建了一个 provider,指向 https://api.marlin-phone.me/v1/,用同样的 token,创建了一个测试智能体。然后让两个智能体对话:
1
2
我 → 测试智能体: "回复我:测试通过 ✅"
测试智能体: (沉默)... 返回空
查日志发现:
1
Provider 'friend_api' not found.
这是一个 provider 加载的 bug,重启后解决了。但接下来才是真正的坑:
1
openai.PermissionDeniedError: Your request was blocked.
锁定真凶:请求头
我写了段脚本,拦截 OpenAI Python 客户端发出的实际请求:
1
2
3
4
5
6
7
8
9
10
class DebugClient(httpx.AsyncClient):
async def send(self, request, **kwargs):
print(f"Headers: {request.headers}")
return await super().send(request, **kwargs)
client = AsyncOpenAI(
base_url="https://api.marlin-phone.me/v1/",
api_key="my_token",
http_client=DebugClient(proxy="http://127.0.0.1:7890"),
)
输出让我眼前一亮:
1
2
3
4
5
6
7
Header [user-agent]: AsyncOpenAI/Python 2.33.0
Header [x-stainless-lang]: python
Header [x-stainless-package-version]: 2.33.0
Header [x-stainless-os]: Linux
Header [x-stainless-arch]: x64
Header [x-stainless-runtime]: CPython
Header [x-stainless-runtime-version]: 3.11.2
Stainless 是 OpenAI 用来生成 Python SDK 的工具链。这些 X-Stainless-* 头是 OpenAI Python 客户端自动加的遥测信息,用来统计 SDK 版本、操作系统、Python 实现等。
我对比测试了一下:
| User-Agent / 请求头 | 结果 |
|---|---|
curl/7.88.1(无 Stainless 头) |
✅ 200 |
AsyncOpenAI/Python 2.33.0(有 Stainless 头) |
❌ 403 |
| 纯 httpx 请求(无 Stainless 头) | ✅ 200 |
Cloudflare 识别到 X-Stainless-* 这些头,断定这是自动化脚本,直接 403 拦截了。
尝试 WAF 规则
我尝试通过 Cloudflare API 创建 WAF 跳过规则:
1
2
3
4
5
6
{
"action": "skip",
"action_parameters": {
"phases": ["http_request_firewall_managed"]
}
}
规则创建成功,但 skip 动作在免费计划上不生效。我又试了 products: ["bic", "uaBlock", "waf"] 等参数,都不行。
最终答案:Browser Integrity Check
在 Cloudflare 的安全设置里,有一个叫 Browser Integrity Check(浏览器完整性检查) 的功能。它会检查请求头是否”像”一个真实浏览器发出的。X-Stainless-* 头显然不像是浏览器会发的,于是被拦截了。
关掉它之后:
1
2
$ curl -H "User-Agent: AsyncOpenAI/Python 2.33.0" ...
# ✅ 200 正常!
根因就是 Browser Integrity Check。
两种解决方案
方案 A:关掉 BIC(推荐给 API 域名)
去 Cloudflare Dashboard → Security → Settings → 关闭 Browser Integrity Check。
对 API 子域名来说是安全的,因为 API 本身有 token 认证。朋友直接配置 URL 和 token 就能用,不需要额外设置。
方案 B:保持 BIC 开启,覆盖请求头
如果不想关 BIC,可以在 QwenPaw 的 provider 配置里加 custom_headers:
1
2
3
4
5
6
7
8
9
10
11
{
"custom_headers": {
"User-Agent": "curl/7.88.1",
"X-Stainless-Lang": "",
"X-Stainless-Package-Version": "",
"X-Stainless-OS": "",
"X-Stainless-Arch": "",
"X-Stainless-Runtime": "",
"X-Stainless-Runtime-Version": ""
}
}
这样 X-Stainless-* 头被覆盖成空字符串,Cloudflare 就认不出来了。
总结
这次排查从”curl 能通,Python 不通”的小问题出发,一步步深挖到 Cloudflare 的 Browser Integrity Check 机制。几个关键教训:
- curl 测试通过不代表所有客户端都能通过——不同客户端有不同的请求头
- 拦截请求头看真实内容——比瞎猜高效得多
- Cloudflare 的免费计划 WAF 跳过规则不生效——别在这上面浪费时间
- API 域名关掉 BIC 通常没问题——API 本身有认证,不需要浏览器完整性检查
如果你也遇到类似问题,先检查一下 Browser Integrity Check 的状态吧。