Skip to content

请求体超过 ~128KB 触发上游 WAF 验证时静默返回空响应(HTTP 200 + completion_tokens: 0,无任何错误) #147

Description

@cheungringo420

现象

当请求体超过约 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')
}

这样至少:

  1. 日志里能看到真实原因;
  2. 客户端收到明确错误而不是「成功的空响应」,可以据此重试、切账号或缩短 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions