从「框架内部报错」到「请求头被网关截断」:一次 Sentry 排障与前端 Cookie 误用复盘

线上 Sentry 突然冒出一类报错,栈顶全是框架内部代码,乍看像是框架本身的 bug。顺着排查下去,根因却落在一个最不起眼的地方------请求头被网关截断;而把请求头撑爆的,是前端往 Cookie 里塞了本不该放的大业务数据。

这篇文章把这次排查做一次通用化复盘:怎么用 Sentry 定位这类「伪框架错误」、网关为什么会截断 header、前端哪些误用会撑大请求头、四种本地存储该怎么选,以及清理历史脏 Cookie 时一个很容易踩的 js-cookie 坑。

一个看起来像框架 bug 的服务端报错

线上一个 SSR 应用(Next.js App Router)报出一条错误:

javascript 复制代码
Error: The router state header was sent but could not be parsed.

栈顶全在框架编译产物里(next-server runtime),没有一行业务代码。第一反应很容易是「框架 bug / 版本问题」。但先别急着甩锅给框架,Sentry 上几个字段能快速澄清这条错误的「身份」:

  • platform = nodeos.name = <某 Linux 发行版>server_name = <某容器实例> → 这是服务端抛的,不是用户浏览器。
  • mechanism = 框架自动捕获handled = no → 框架在请求处理链路里自己抛出并上报的,不是业务 try/catch。
  • Users Impacted = 0 → 服务端错误没有用户级上下文(不代表没影响用户,只是这条记录里没绑定用户)。

看到「栈顶在框架 runtime + 服务端 + 自动上报」,先把它当成请求处理链路的问题,而不是某个业务分支的 bug。

用 Sentry 把范围收敛:三个聚合维度

要判断「是不是某次发版引入的」,最快的办法是按维度聚合事件,而不是一条条看 stack:

  • 按 release 聚合 :错误集中在某一两个版本 → 大概率版本相关(回归 / 新老版本错配);如果横跨所有版本同时发生 → 多半是环境 / 基础设施层面的系统性问题,与某次发版无关。
  • 按 url / 路由聚合:集中在特定路由,还是全站铺开?全站铺开 + 服务端,更像基础设施而非某个页面的代码。
  • 按 environment / browser 聚合:确认影响面与人群。

这次的数据是:跨所有 release、覆盖全部路由、全部发生在服务端 。三个维度叠加,结论很清楚------不是某次发版的锅,问题在请求到达应用之前的链路上,也就是网关。

经验法则:栈顶在框架 runtime、跨版本、服务端、全站------优先怀疑请求链路(网关 / CDN / 反向代理),而不是业务代码。

网关为什么会截断 header

HTTP 请求头不是无限大的。几乎所有反向代理 / 网关 / WAF 都对 header 大小有上限。以 nginx 为例:

  • large_client_header_buffersclient_header_buffer_size 控制单个请求头行与缓冲区大小,常见默认是 8KB 量级
  • 超过上限时,典型行为有两种:直接拒绝 (返回 431 Request Header Fields Too Large400 Bad Request),或在某些链路上截断后继续转发。
  • 一旦某个长 header 被截断成残缺值,后端再去解析它就会失败------于是你在应用层看到的是「解析失败」,但破坏其实发生在更早的网关层。

SSR 框架对此尤其敏感:现代框架在软导航 / 预取时会带上较长的内部请求头。比如 Next.js 的 Next-Router-State-Tree 编码了当前路由树,层级越深这个头越长。它本身加上其它请求头叠加在一起,一旦总量越过网关上限,后端拿到的就是残缺头 → 解析报错。

flowchart LR A[浏览器 携带超大 Cookie] --> B[网关 检查 header 大小] B --> C[超过上限 截断或返回 4xx] C --> D[SSR 读取路由状态头失败] D --> E[Sentry 服务端报错]

关键点:应用层的「解析失败」往往只是表象,真正的破坏在网关层;而把总 header 撑到超限的,常常是前端。

前端的隐形元凶:什么在悄悄撑大请求头

最容易被忽视的,是 Cookie

  • Cookie 会随每个同域请求自动塞进请求头------你不显式发送,它也跟着走。
  • 一旦往 Cookie 里写大业务数据(聊天的首条消息、表单草稿 JSON、缓存的接口结果......),或者按会话不断累积一批 Cookie,请求头就会持续变大,直到某天突破网关上限。
  • 其它来源:fetch / axios 拦截器里无脑追加的自定义头、超大的 Authorization、把状态塞进 URL 又被某层网关写进头,等等。但 Cookie 是最隐蔽的,因为它是自动发送的,写的时候你根本不会联想到「请求头」。

一句话概括这类误用:把本不需要服务端读取的大数据放进了 Cookie。它带来的「自动随请求发送」对这些数据是纯粹的负担。

本地存储四选一:关键看「会不会进请求头」

很多人选本地存储时只看「容量」和「是否持久」,但对这个问题,最关键的维度是会不会自动进请求头------只有 Cookie 会。

方案 容量(量级) 随请求自动发送 读写方式 作用域 生命周期 服务端可读 适用场景
Cookie ~4KB / 条 ✅ 每个同域请求 同步 domain + path,可跨子域 可设过期时间 需服务端在请求时读取的标识(登录态 / 语言 / 灰度 / AB)
localStorage ~5--10MB 同步(阻塞主线程) 同源,跨标签页共享 持久,需手动清 较大的客户端持久数据(非敏感、不需服务端)
sessionStorage ~5--10MB 同步 同源,单标签页隔离 标签页关闭即清 单标签页临时态(表单草稿 / 向导步骤)
IndexedDB 数百 MB ~ GB(按磁盘配额) 异步(事务 / Promise) 同源,跨标签页共享 持久,需手动清 大量 / 结构化 / 二进制数据(消息缓存 / 离线数据 / 文件)

选型可以简化成几条:

  • 需要服务端在请求时读到的小标识(登录态、语言、灰度、AB 分流)→ Cookie,并严格控制大小。
  • 客户端用、量较大、不需服务端 → localStorage 或 IndexedDB。
  • 只在单个标签页临时用、关掉就该消失 → sessionStorage。
  • 大量 / 结构化 / 二进制 / 需要按 key 查询 → IndexedDB。

反过来记一条红线:凡是不需要服务端读取的数据,都不要放 Cookie,大数据更是绝对禁止。Cookie「自动随每个请求发送」的特性,对这类数据只有坏处。

迁移本身不难------把大值从 Cookie 改存到 IndexedDB 就行。但有一个常被漏掉的收尾动作:只改了「新写入」,没清「旧残留」

用户浏览器里早先写入的大 Cookie 不会自己消失,它会继续随每个请求发送,问题照旧。尤其当一段流量已经切到新代码、另一段还在老代码时,老用户携带的历史脏 Cookie 会一直把请求头顶在高位。

清理历史脏 Cookie 的实践:

  • 时机:应用启动早期(客户端),一次性扫描并删除已废弃前缀的 Cookie。
  • 幂等、无副作用:这些 Cookie 已经没有任何读取方,删除是安全的。
  • 写法:JS 没有「删除 Cookie」的 API,只能用「空值 + 过去的过期时间」覆盖,告诉浏览器立即删掉它。
js 复制代码
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
  • domain / path 必须与写入时一致 才能命中删除。来源未知时,可对几种常见组合(host-only、主域 .example.com)都写一遍覆盖删除。不用担心污染或异常 :已过期的 Set-Cookie 浏览器不会真正存储,所以不匹配的尝试只是无操作;document.cookie 的 setter 对非法输入是静默忽略 (不像 localStorage 会抛异常)。
  • 局限与兜底 :客户端清理对每个用户的首个请求 无效(Cookie 已经在入站请求里了),之后才会变干净。要在首个响应就清,可以在服务端用响应的 Set-Cookie 把它置为过期,或在中间件里处理。网关 header 上限可以适当调高作为兜底,但那是止血而非根因修复------根因永远是「谁把大值放进了 header」。

项目里通常用 js-cookie 这类库管理 Cookie。但删除特殊名字 的 Cookie 时,它可能静默失败

js-cookie 在写入 / 删除时会对 Cookie 做 URL 编码(encodeURIComponent 再做一次有限的反转义白名单)。如果名字里含 @:(), 这类字符,就会被编码:

js 复制代码
Cookies.remove('@app:first-msg:123');
// 实际写出的删除项名字 ≈ %40app%3Afirst-msg%3A123

而浏览器里这条 Cookie 的真实名字是字面@app:first-msg:123(如果它当初是用别的方式 / 别的库以字面名写入的)。两个名字对不上------删除项指向了一个根本不存在的 Cookie → 删了个寂寞,原 Cookie 还在。

这正是清理这类历史脏 Cookie 时要用 raw document.cookie 写字面名的原因。两条可复用经验:

  • 特殊字符命名的 Cookie (含 @ : ( ) , 等),读写 / 删除前先确认编码两端一致;不一致时直接用 document.cookie 写字面名。
  • 跨库 / 跨历史数据:用 A 写、用 B 删,要警惕两者的「名 / 值编码策略」不同导致的不匹配。这类问题在编辑器预览、本地都看不出来,只有线上真实数据才暴露。

小结:一份可复用的 checklist

  • 线上出现「框架内部」报错:先用 Sentry 的 platform / os / server_name 分清 client / server,再按 release 聚合排除版本错配;跨版本 + 全站 + 服务端 → 怀疑网关 / 请求链路
  • header 被截断或 4xx:检查网关 header 上限,但根因常在前端把大值放进了 header(尤其是 Cookie)。
  • 本地存储选型:记住「只有 Cookie 会自动进请求头」;不需服务端读取的大数据,一律不进 Cookie。
  • 迁移存储后:主动清理历史脏 Cookie,别只改写入不删旧值。
  • 删 Cookie:空值 + 过去过期时间 + 匹配 domain/path;特殊字符名要警惕 js-cookie 编码导致的「删不掉」。

很多线上「疑难杂症」最后都收敛到一个朴素的工程原则上:请求头是稀缺资源,别往里塞业务数据

参考与数据源

相关推荐
云水一下20 分钟前
Vue.js从零到精通系列(五):全局状态管理——Pinia 核心与实践
前端·javascript·vue.js
我不是外星人28 分钟前
浅谈我对 AI 发展的看法
前端·ai编程·claude
甲维斯1 小时前
测一波Kimi K2.7,消耗一周配额!
前端·人工智能·游戏开发
Dick5071 小时前
ROS2 多机器人通用 Driver 层复盘:BaseRobotDriver 到多平台 Mock 切换实现
前端·javascript·机器人
xiaofeichaichai2 小时前
前端安全 XSS 与 CSRF
前端·安全·xss
JS菌2 小时前
Skills 动态加载系统:让 AI Agent 按需获取领域知识
前端·人工智能·后端
weedsfly2 小时前
Sass 代码复用完全指南:从变量到模块化
前端
张拭心2 小时前
Android 17 新特性:后台音频交互限制加强
android·前端
张拭心2 小时前
Android 17 新特性:ProfilingManager 新触发器
android·前端