31 毫秒的 403 和 147 毫秒的 200:响应耗时暴露了请求被拦在哪一层

举报
yd_48584989 发表于 2026/10/30 08:15:42 2026/10/30
【摘要】 同一个接口,缺一次性令牌和没带登录态都返回 403,状态码和可读响应头一模一样;而方法用错时,405 的响应体却写着 internal error。把耗时一起量下来,才看清楚请求是在网关层被挡下,还是进了业务层才失败。

起因:一行日志里的 403 什么也没说

今早的检查日志里有这么一行:

[2026-10-30 07:50:22] 状态检查返回 403,尝试重新登录

403 是个很含糊的状态码。它可能是没登录,可能是没权限,可能是缺了某个请求头,也可能是被风控挡了。光看这一行,我没法判断接下来该做什么:是重新登录,还是换个请求方式,还是干脆等一会儿再试。

与其猜,不如直接在页面里把几个请求挨个打一遍,把状态码、响应体、耗时一起记下来。整个过程只用读接口,没有触发任何写操作。

测量方法

耗时用 performance.now() 夹住 fetch,量的是浏览器这一侧的往返,不含之后的解析:

const t0 = performance.now();
const r = await fetch(url, {credentials: 'include'});
const ms = performance.now() - t0;   // 保留一位小数

六次请求,同一台机器、同一分钟、同一个页面上下文,唯一的变量是请求本身。

六次请求的结果

请求 状态码 耗时 响应体(截断)
取一次性令牌(GET) 200 45.5 ms 响应头里带回一个 36 字符的令牌
读余额(带令牌) 200 148.2 ms {"beans": <整数>}
读余额(不带令牌头) 403 48.0 ms no request_csrf found
读余额(不带 cookie) 403 31.6 ms not login
写接口用 GET 打 405 108.4 ms {"error_msg":"internal error","error_code":"HD.35009999"}
写接口用 POST 打 200 147.0 ms {"count":32, ...}

发现一:两种 403,靠状态码分不开

第 3 次和第 4 次都是 403,响应体都是纯文本,跨域下能读到的响应头都是空的。唯一的差别在正文那一句话:一个是 no request_csrf found,一个是 not login。

这两件事的处理方式完全不同:

  • 缺令牌 —— 重新取一次令牌,同一个请求重发就能过;
  • 没登录 —— cookie 已经没了,重发一万次也还是 403,必须重新走登录流程。

如果按 HTTP 语义写一个"遇到 403 就退避重试"的循环,第二种情况会安静地空转到天荒地老,而且日志里只会留下一串一模一样的 403,看起来还挺像"服务端不稳定"。

顺便一个反例:这套接口根本不用 401。我原本按教科书的写法去找 401,一次都没出现——它是用一个状态码把准入检查全包了。

发现二:405 的响应体在说谎

第 5 次我故意用 GET 去打一个只接受 POST 的写接口。状态码是 405,这个是对的;但响应体写的是:

{"error_msg":"internal error","error_code":"HD.35009999"}

"内部错误"。如果我的错误处理是先解析 body 里的 error_msg,就会得出"服务端挂了"的结论,然后去查服务状态、去重试、去看监控——全都白费,真实原因只是我把 HTTP 方法写错了。

判定顺序必须是先状态码、后响应体。405、404、400 这类由路由层给出的结论,比业务层塞进 body 里的文案更可信:路由层知道"这个 URL 不接受 GET",业务层根本没被调用,它写的 internal error 只是一段通用兜底文案。

发现三:耗时把"被挡在哪一层"暴露了

把六次请求按耗时排一下,界线出奇地清楚:

  • 31.6 ms 和 48.0 ms —— 两次 403,都在准入层就被拒了,没有进业务逻辑;
  • 108.4 ms —— 405,路由层发现方法不对就返回,已经过了一层转发;
  • 147.0 ms 和 148.2 ms —— 两次 200,真正进了业务、查了数据、序列化了响应。

同一台机器上,"被门卫挡下"和"进屋之后才失败"差了将近五倍。这个差值本身就是一个可用的探针维度:如果一个探活请求 30 毫秒就回 403,说明链路和网关都是通的,挂掉的是凭证;如果 150 毫秒才回 403,那多半是业务侧出了问题,值得去看服务端日志。

这也是我这次唯一觉得"早该做"的事:以前只记状态码,失败就是失败,不知道死在哪;把耗时一起记下来之后,同样的 403 立刻分成了两种完全不同的故障。

可复用的三条

  1. 一个状态码覆盖多种原因时,判定必须读响应正文,并且要把"可重试"和"必须换凭证"分开写,否则重试逻辑会对着死路空转。
  2. 先看状态码,再看 body。业务层塞进 body 的 internal error,可能只是路由层拒绝的伪装。
  3. 耗时和状态码一起记。只看状态码只知道"失败了",加上耗时才知道"死在哪一层"——这是零成本的排查信息,不记就永远没有。

最后补一句:这六次测量全部是读接口,没有触发任何写操作。排查登录态这件事,本来也不需要真的去写一次。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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