首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >点了发送不报错、评论就是不出现:两次误判后,我用页面自带的签名函数问出了 code 300011

点了发送不报错、评论就是不出现:两次误判后,我用页面自带的签名函数问出了 code 300011

原创
作者头像
用户12800671
发布于 2026-10-26 20:45:27
发布于 2026-10-26 20:45:27
190
举报

先说结论

一个「点按钮什么都没发生」的故障,我判断错了两次。

第一次以为是输入没进框,第二次以为是点击没命中。两次都改对了代码,故障依旧。真正的答案不在前端:请求发出去了,被服务端静默丢掉,一个字的提示都不给。

最后把这层捅破的办法不是逆向签名算法,而是借页面自己已经加载好的签名函数,在页面上下文里算出签名,再拿它调接口,让服务端把话说清楚——它说的是 code 300011,当前账号存在异常。

一句话总结:前端「没反应」不是证据,服务端「说了什么」才是证据。 前者只能证明你没看到,后者才能证明发生了什么。

现象:三次「成功的失败」

场景是一条评论回复自动化:打开评论区,找到待回复的评论,输入文字,点发送。

现象是这样:点发送后,输入框正常收起,没有 toast,没有红色提示,控制台无报错。看起来一切顺利。刷新页面,评论不存在。

同一段代码重复五次,五次都这样。这是最难查的一类故障——它长得像成功:如果是接口 500,十分钟就能定位;它连 500 都不给。

第一次误判:以为输入没进框

第一反应是输入没生效。这个方向有充分的历史依据:现在的前端框架(React、Vue)都是受控组件,输入框的 value 由框架的 state 驱动。你用 element.value = 'xxx' 直接赋值,DOM 上确实能看到文字,但框架的 state 还是空的。点提交时,它提交的是 state,不是 DOM。

这个坑我踩过,所以第一版代码用的是「原生 setter + 事件派发」的老办法:

代码语言:bash
复制
// 老办法:原生 setter 破解受控组件
const setter = Object.getOwnPropertyDescriptor(
  HTMLTextAreaElement.prototype, 'value'
).set;
setter.call(textarea, '要回复的内容');
textarea.dispatchEvent(new Event('input', { bubbles: true }));

后来还加了一版 document.execCommand('insertText')——更古老,但更「像人打字」,很多富文本编辑器只认它。

两版都失败,表现一模一样:框里有字,点发送,字没了,评论没出现。

输入层排除:字确实进了框,肉眼可见。

第二次误判:以为点击没命中

第二个怀疑对象是点击。这个按钮可能被浮层盖住,可能是假按钮,也可能被框架的合成事件拦掉。于是先把 DOM 摸清楚,从评论容器一路往下打:

代码语言:bash
复制
// 摸结构:容器 → 操作区 → 回复按钮 → 编辑器 → 提交按钮
.main > .info > .actions > .action-reply
textarea.comment-input
button.submit

结构清楚,button.submit 存在且唯一。换成 CDP 真实键击,不再用 el.click():

代码语言:bash
复制
// 用 CDP 发真实键击:文本逐字入框,不是赋值
Input.insertText  ->  文本成功出现在 textarea 里
Input.dispatchMouseEvent (mousePressed / mouseReleased)  ->  点在 button.submit 的坐标上

结果:文本确实逐字入框了,点击也确实命中了——因为点击之后输入框收起了。

请注意这个细节。输入框收起,说明按钮的 handler 真的执行了,而且走到了「提交成功后清空并关闭编辑器」这条路径。如果点击没命中,框会一直开着。

所以请求一定发出去了。 到这里,前端的两层(输入、点击)全部排除。

转折:让服务端自己说话

问题变成:请求发出去了,服务端回了什么?

直接抓包看不到,因为这个接口带签名。请求头里有时间戳和签名对(类似 X-S、X-T 这种),签名由页面加载的一段 JS 计算,其中掺了运行时变量。硬啃这段 JS 是典型的投入产出失衡——它会被更新,而且我只需要读一次错误码,不需要长期调用。

这里有个更省事的思路:页面里已经有一个能算签名的函数,为什么还要自己算?

这类平台通常会在 window 上挂一个签名入口(风控脚本初始化时挂的),页面自己发请求时就调它。于是步骤变成三步,全部在页面上下文里完成:

  1. 在页面里调用这个签名函数,传入接口路径和请求体,拿到签名对;
  2. 带着签名对,在页面上下文里发一次 fetch(同源、带 cookie,天然合法);
  3. 把服务端返回的原始 JSON 原样拿出来。
代码语言:bash
复制
// 在页面上下文里求值:先算签名,再发请求,只读一次
const sign = window.<页面自带的签名函数>(path, data);
const r = await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', ...sign },
  body: JSON.stringify(data)
});
return await r.json();   // 原样返回,不做任何解析或美化

跑通之后,服务端回的原话是:

代码语言:json
复制
{ "code": 300011, "msg": "当前账号存在异常" }

一个数字,一句中文。前面两个小时的所有猜测,都被这七个字符替代了。

为什么前端会「静默丢弃」

风控类接口的设计目标不是「告诉用户你错了」,而是「不要给爬虫任何反馈信号」。所以它对异常账号的处理是:接受请求,返回一个前端不认识的错误码;前端的分支里没有这个码,于是什么都不做。

前端代码大概是这么写的:成功就清空关闭,失败就弹个 toast,但它只处理自己认识的那几个失败码。遇到 300011,if 全落空,handler 直接结束——框已经清了,提示没有弹。

于是「静默失败」不是 bug,是设计。 你感受到的「成功」,是它故意留给你的假象。

这也解释了前面两次误判为什么都是「改对代码、故障依旧」:我修的都是真实存在的问题(受控组件、点击命中),只是它们不是这次的因。修对了旁边的东西,主因一动没动。

追根:这个异常状态是我自己造的

拿到 300011 之后,往前翻日志,链条很清楚:

  • 当天上午,同一个账号在另一台机器上触发过「电脑设备登录超限」;
  • 诱因是我这边的「登录保活」脚本短时间内连做了三次登录态检查;
  • 平台的处理是服务端作废会话:cookie 还在本地(看着还有一年多),服务端已经不认了。

这三条串起来,就是完整的因果:高频登录检查 → 设备数风控 → 服务端作废会话 → 所有写操作被静默丢弃。

最讽刺的是:那个「保活」脚本本意是防掉线,结果它自己就是掉线的原因。为了确认「我还活着」,它把自己弄死了。

处置:停手比修代码更有效

知道是账号级状态之后,处置方式是反直觉的——什么都不做。

因为这类风控的计分是累加的:每重试一次就多一条异常记录,解冻时间往后推。继续「优化代码再试一次」,大概率是在给处罚加码。

所以做了三件事,没有一件是改代码:

  1. 停掉网页端一切提交动作,冷却静置。只留只读探测,频率降到 2 小时一次。
  2. 降级执行:10 条待回复的评论全部写成手机可直接粘贴的文案,落到本地文件里。自动化干不了的,人工两分钟能干完——不要为了「全自动」三个字把事卡在那儿。
  3. 把判据从「现象」换成「服务端码」:恢复条件不再是「页面看着正常了」,而是「只读探测拿到 code=0」。探到就自动按队列补发,每次不超过 5 条、间隔 25 秒;探不到就继续等,不猜。

第 3 条最值钱。原来的恢复判据是「页面能打开评论区」「能读到自己的头像」——这两件事在会话被作废之后依然成立,因为读接口不一定校验会话。判据选错,监控就永远报绿。

可复用的诊断顺序

把这次的路径抽出来,是一个固定顺序,适用于所有「点了没反应」的场景:

层

要回答的问题

判据(必须是可观测事实)

输入层

文字真的进框了吗

框内文本可见;不是赋值成功,是渲染后仍在

交互层

按钮真的被触发了吗

有副作用发生(框收起/按钮置灰/出现 loading)

传输层

请求真的发出去了吗

有请求记录;或副作用证明 handler 走到了提交分支

服务端

服务端说了什么

原始 JSON,不做任何解析

前三层都可能骗你,第四层不会。所以排查顺序该反过来:先想办法拿到第四层,再回头解释前三层。 这次我按顺序排查,白花两小时;一开始就去要服务端原话,十分钟能定位。

三条带走的经验

一、「没有报错」和「没有错误」是两件事。 前者描述你的观测能力,后者描述系统状态。两者之间的差距,就是风控、静默降级、前端兜底 catch 刻意留出的空间。看到「没报错」,先问自己的观测点在哪一层,而不是默认「那应该是好的」。

二、需要签名才能调的接口,不一定要自己实现签名。 页面里已经有一个算好的函数时,借它一次比逆向它一周划算。前提是只用来读诊断信息——这次全程只读,没做任何写操作。

三、监控判据要选在「会先坏掉」的那一层。 原来监控「页面能打开」,这层最后才坏;改成监控「写接口返回 code=0」,这层最先坏。判据往前挪一层,故障早发现一天。

最后补一句不太好听的:这次故障里,代码一行没错。错的是我配给它的那个「保活」动作,以及我用页面表象去推断服务端状态。自动化系统里最贵的成本往往不是写错的代码,是判断错的前提——它不报错,只会安静地跑下去。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 先说结论
  • 现象:三次「成功的失败」
  • 第一次误判:以为输入没进框
  • 第二次误判:以为点击没命中
  • 转折:让服务端自己说话
  • 为什么前端会「静默丢弃」
  • 追根:这个异常状态是我自己造的
  • 处置:停手比修代码更有效
  • 可复用的诊断顺序
  • 三条带走的经验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档