现象
当请求体超过约 128 KB 时,上游 chat.qwen.ai 会返回 WAF 反爬验证响应,但 Qwen2API 会静默丢弃它,最终给客户端返回 HTTP 200 + 空内容 + completion_tokens: 0,没有任何错误信息(日志里也看不到)。
调用方完全无法区分「模型没话说」和「被 WAF 拦了」。我们排查了很久才定位到,因为表面上看起来像是 token 上限问题,实际上不是。
根因
上游返回的是:
{"ret":["FAIL_SYS_USER_VALIDATE","RGV587_ERROR::SM::哎哟喂,被挤爆啦,请稍后重试"],
"data":{"url":"https://chat.qwen.ai:443//api/v2/chat/completions/_____tmd_____/punish?x5secdata=...&x5step=2&action=captcha&pureCaptcha="}}
这个 JSON 没有 choices 字段,于是在 src/controllers/chat.js 两处流式解析里被直接跳过(当前 main 866d429):
// 第 260-263 行(流式)
const decodeJson = isJson(dataContent) ? JSON.parse(dataContent) : null
if (decodeJson === null || !decodeJson.choices || decodeJson.choices.length === 0) {
return // <-- captcha 响应在这里被丢弃
}
// 第 576-579 行(非流式)同样逻辑
if (decodeJson === null || !decodeJson.choices || decodeJson.choices.length === 0) {
continue // <-- 同上
}
丢弃之后,代码继续走到结尾用 tiktoken 自行计算 usage 并输出,所以客户端拿到的是一个「成功」的空响应。
全项目搜索 FAIL_SYS_USER_VALIDATE / RGV587 / punish / x5sec 均无任何处理。
复现
直接对 chat.qwen.ai 发请求,每种情况跑 3 次,结果完全稳定(非随机):
| 内容 |
字符数 |
请求体大小 |
结果 |
| ASCII |
55,000 |
54 KB |
✅ 正常 |
| ASCII |
120,000 |
117 KB |
✅ 正常 |
| CJK |
40,000 |
117 KB |
✅ 正常 |
| CJK |
44,000 |
129 KB |
❌ captcha |
| ASCII |
200,000 |
196 KB |
❌ captcha |
注意 ASCII 120,000 字符和 CJK 40,000 字符字符数差 3 倍但字节数相同(都是 117 KB),行为也相同 —— 所以触发条件是请求体字节数,阈值在 128 KB 左右,和 token 数、字符数、模型都无关(qwen3.8-max / qwen3.7-max / qwen3.7-plus / qwen3.6-plus 以及 -thinking / -search / -thinking-search 各变体表现一致)。
顺带一提:gzip 压缩请求体不能绕过。
建议修复
不需要处理 captcha 本身,只要别把它吞掉就够了。在 processSSEPayload(以及非流式那处)里,在 !decodeJson.choices 提前返回之前加一个判断:
// 上游 WAF 反爬验证:没有 choices,但必须让调用方知道
if (decodeJson && Array.isArray(decodeJson.ret) &&
decodeJson.ret.some(r => typeof r === 'string' && r.includes('FAIL_SYS_USER_VALIDATE'))) {
logger.error('上游触发 WAF 验证(请求体过大或需要 captcha)', 'CHAT', '', {
ret: decodeJson.ret,
punishUrl: decodeJson.data && decodeJson.data.url
})
throw new Error('upstream_captcha_required')
}
这样至少:
- 日志里能看到真实原因;
- 客户端收到明确错误而不是「成功的空响应」,可以据此重试、切账号或缩短 prompt。
如果愿意再进一步,可以考虑在 README 里注明请求体 128 KB 的实际上限 —— 这一点目前完全没有文档说明,而模型元数据里写的是 max_context_length: 1000000,很容易误导(我们一开始就以为是 context window 的问题)。
我可以提 PR,如果你觉得这个方向可以的话。
English summary
When the request body exceeds ~128 KB, upstream chat.qwen.ai returns a WAF anti-bot challenge (FAIL_SYS_USER_VALIDATE / RGV587_ERROR, with a punish?...&action=captcha URL). That JSON has no choices, so src/controllers/chat.js (lines 260-263 and 576-579 on main 866d429) silently discards it and the client receives HTTP 200 with empty content and completion_tokens: 0, with no error logged anywhere.
The trigger is request body bytes, not tokens or characters — ASCII 120,000 chars and CJK 40,000 chars are both 117 KB and both succeed; 129 KB fails. Deterministic across 3 runs and identical for every qwen 3.6/3.7/3.8 model and variant. gzip does not bypass it.
Suggested fix: detect ret containing FAIL_SYS_USER_VALIDATE before the !choices early-return, log it, and surface an error to the caller. Happy to open a PR.
现象
当请求体超过约 128 KB 时,上游
chat.qwen.ai会返回 WAF 反爬验证响应,但 Qwen2API 会静默丢弃它,最终给客户端返回HTTP 200+ 空内容 +completion_tokens: 0,没有任何错误信息(日志里也看不到)。调用方完全无法区分「模型没话说」和「被 WAF 拦了」。我们排查了很久才定位到,因为表面上看起来像是 token 上限问题,实际上不是。
根因
上游返回的是:
{"ret":["FAIL_SYS_USER_VALIDATE","RGV587_ERROR::SM::哎哟喂,被挤爆啦,请稍后重试"], "data":{"url":"https://chat.qwen.ai:443//api/v2/chat/completions/_____tmd_____/punish?x5secdata=...&x5step=2&action=captcha&pureCaptcha="}}这个 JSON 没有
choices字段,于是在src/controllers/chat.js两处流式解析里被直接跳过(当前 main866d429):丢弃之后,代码继续走到结尾用 tiktoken 自行计算 usage 并输出,所以客户端拿到的是一个「成功」的空响应。
全项目搜索
FAIL_SYS_USER_VALIDATE/RGV587/punish/x5sec均无任何处理。复现
直接对
chat.qwen.ai发请求,每种情况跑 3 次,结果完全稳定(非随机):注意 ASCII 120,000 字符和 CJK 40,000 字符字符数差 3 倍但字节数相同(都是 117 KB),行为也相同 —— 所以触发条件是请求体字节数,阈值在 128 KB 左右,和 token 数、字符数、模型都无关(
qwen3.8-max/qwen3.7-max/qwen3.7-plus/qwen3.6-plus以及-thinking/-search/-thinking-search各变体表现一致)。顺带一提:
gzip压缩请求体不能绕过。建议修复
不需要处理 captcha 本身,只要别把它吞掉就够了。在
processSSEPayload(以及非流式那处)里,在!decodeJson.choices提前返回之前加一个判断:这样至少:
如果愿意再进一步,可以考虑在 README 里注明请求体 128 KB 的实际上限 —— 这一点目前完全没有文档说明,而模型元数据里写的是
max_context_length: 1000000,很容易误导(我们一开始就以为是 context window 的问题)。我可以提 PR,如果你觉得这个方向可以的话。
English summary
When the request body exceeds ~128 KB, upstream
chat.qwen.aireturns a WAF anti-bot challenge (FAIL_SYS_USER_VALIDATE/RGV587_ERROR, with apunish?...&action=captchaURL). That JSON has nochoices, sosrc/controllers/chat.js(lines 260-263 and 576-579 on main866d429) silently discards it and the client receives HTTP 200 with empty content andcompletion_tokens: 0, with no error logged anywhere.The trigger is request body bytes, not tokens or characters — ASCII 120,000 chars and CJK 40,000 chars are both 117 KB and both succeed; 129 KB fails. Deterministic across 3 runs and identical for every qwen 3.6/3.7/3.8 model and variant. gzip does not bypass it.
Suggested fix: detect
retcontainingFAIL_SYS_USER_VALIDATEbefore the!choicesearly-return, log it, and surface an error to the caller. Happy to open a PR.