心跳接口返回 404 却不报错:一次 session_id 轮转陷阱的排查

举报
yd_216677863 发表于 2026/11/09 09:15:39 2026/11/09
【摘要】 同一个心跳接口第一次 200、第二次 404,日志却不报错。根因是签名 token 在服务端每次重新签发,调用方必须接力;附带累计时长按真实时间差计算、中途重建会清零两个坑。

做签到类自动化时遇到一个很反直觉的现象:同一个接口,第一次调用 200,第二次就 404 session_not_found,而三次调用的代码完全一样。日志里既没有异常也没有报错,只是 accumulated_ms 永远停在两位数,任务永远不达标。

这类问题的共同点是:接口是有状态的,而调用方把它当成了无状态的。本文把排查顺序和最终结论摊开。

一、先确认 404 不是网络层的问题

第一步永远是看响应体而不是状态码。404 在很多网关里会被渲染成 HTML 错误页,而这个接口返回的是结构化 JSON:{"detail":"session_not_found"}。能返回结构化错误,说明请求已经打到了业务层,路由也匹配上了,只是服务端找不到你给的那个 session。

这就排除了三类常见原因:域名解析、反向代理路径前缀错误、以及鉴权失败(鉴权失败通常是 401 或 403,而不是 404)。

二、把「创建」和「心跳」分开看

这类接口通常是两段式:

POST /api/growth/tasks/case-view-daily/sessions -> 返回 {session_id, today:{need_seconds:30, max_gap_ms:15000}}

POST /api/growth/sessions//heartbeat -> 返回 {session_id, accumulated_ms, completed, award_status}

注意心跳的返回体里也有一个 session_id。绝大多数人(包括我)会把它当成「回显」直接忽略掉,继续用第一次拿到的那个。这就是致命点。

三、真正的机制:签名体在轮转

把两次返回的 session_id 各自 base64 解码,就看得出来。它不是一个随机 ID,而是一段带签名载荷的 token:

{"accumulated_ms":3108,"case_id":null,"domain_id":"f3bbe07c...","domain_name":"hid_xxx","exp":1794012...}

每次心跳服务端都会重算累计时长、刷新过期时间,然后用新载荷重新签一个 token 返回。旧 token 对应的记录在服务端已经被替换,所以下一次拿旧 token 去打就是 404。

这不是设计缺陷,而是「无状态服务 + 客户端持有状态」的典型做法:服务端不存 session 表,所有进度都压在这枚 token 里。代价就是调用方必须严格接力。

正确的循环长这样:

let sid = null;

// 只在开始时创建一次

const r = await fetch("/api/growth/tasks/case-view-daily/sessions", {method:"POST", credentials:"include"});

sid = (await r.json()).session_id;


for (let i = 0; i < 12; i++) {

await sleep(3400);

const h = await fetch(`/api/growth/sessions/${encodeURIComponent(sid)}/heartbeat`, {

method: "POST", credentials: "include", headers: {"Content-Type":"application/json"}, body: "{}"

});

const j = await h.json();

if (j.session_id) sid = j.session_id; // <- 关键:接力

if (j.completed) break;

}

四、第二个坑:累计时长按真实时间差算

修好接力之后我发现 accumulated_ms 涨得比预期慢,于是试着在 body 里塞 {"accumulated_ms": 5000},想一次跳 5 秒。结果返回的还是 97。

也就是说服务端完全忽略请求体,用的是服务端自己两次请求之间的时间差。这个设计其实是对的——否则客户端可以随意伪造学习时长。但对调试很不友好:你会以为参数没传对,实际上是参数根本不生效。

配套的还有 max_gap_ms: 15000。相邻两次心跳间隔超过 15 秒,中间那段直接不计入。所以别想着「隔一分钟点一次」凑时长,必须按秒级节奏打。

实测 3.4 秒一跳最稳:既不会因为网络抖动超过 15 秒的空档上限,也不会在一次执行窗口里超时。

五、第三个坑:中途重建会清零

我在中途一度怀疑 session 坏了,于是又调了一次 sessions 接口重新拿一个。结果 accumulated_ms 从 18000 掉回 97。

因为 sessions 是「创建」语义,不是「获取」语义。重新创建等于换了一枚全新的 token,进度天然为 0。判断依据很简单:如果重新创建返回 200 而不是 409,说明之前那个已经废了;如果返回 409 already_completed_in_period,说明今天已经达标,根本不用再跑。

409 也不要理解成错误。它是一条有效信息:别再打了。

六、给这类接口的通用排查顺序

1. 看响应体而不是状态码,确认错误来自业务层还是网关。

2. 把链路上的每个 token 都 base64 解一遍,看它是不是签名体。

3. 逐个比对响应里出现的字段和请求里用到的字段,找「被忽略但其实是新值」的那个。

4. 用固定间隔打三次,记录每次的累计值,判断是「不累加」还是「累加但被重置」。

5. 故意在请求体里传伪造值,如果返回不变,说明服务端按自己的时间算。

6. 中途重新创建一次,看返回 200 还是 409,确定当前 session 是否还有效。

这六步下来,基本能定位到是「接力断了」「间隔超限」还是「重建清零」。三种原因表现都是「进度不涨」,但修法完全不同。

七、小结

这个 bug 最难的地方不是修复,而是它不报错。404 藏在 JSON 里,累计值卡在一个看起来「正常」的小数字上,日志干干净净。如果没有对比响应字段,很容易得出「接口不稳定」这个错误结论,然后加一堆重试——而重试恰好是最糟的做法,因为每重试一次创建就清零一次。

遇到有状态接口,先问一句:这个响应里有没有我没接住的字段。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。