侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 212 篇文章
  • 累计收到 7 条评论

别把 req.headers 当成原始报文

2026-10-19 / 0 评论 / 9 阅读

别把 req.headers 当成原始报文

请求里同一个 HTTP 头出现两次,Node 的 req.headers 不会照原样留两行。字段名被转成小写,重复值还会按字段种类采取不同处理。若日志、签名校验或排错需要知道客户端究竟发了什么,只看这个对象就可能少掉信息。

我更愿意把它当成方便业务读取的视图,而不是收到报文的副本。这个区分听起来小,碰到代理层改写字段时却很实际:应用日志显示的可能是 Node 整理后的值,不是线上的拼写与排列。

同名头并非一律用逗号连接

本机通过 TCP socket 手工发出重复头,再看 Node HTTP server 解析后的三个入口。x-trace 被合并为一个逗号分隔字符串;cookie 用分号加空格连接;set-cookie 则保留成数组。content-type 也有两行,但普通 headers 只留下第一行。字段名大小写则全部归一成小写。

这些输出来自 Node v26.8.1 的实际请求,不是把头对象自己拼出来:

NODE v26.8.1
headers[x-trace] = "first, second"
headers[cookie] = "a=1; b=2"
headers[set-cookie] = ["a=1","b=2"]
headers[content-type] = "text/plain"
headersDistinct = {"host":["localhost"],"x-trace":["first","second"],"cookie":["a=1","b=2"],"set-cookie":["a=1","b=2"],"content-type":["text/plain","application/json"],"connection":["close"]}
rawHeaders = ["Host","localhost","X-Trace","first","x-trace","second","Cookie","a=1","cookie","b=2","Set-Cookie","a=1","set-cookie","b=2","Content-Type","text/plain","content-type","application/json","Connection","close"]

这三种表现不能互相外推。x-trace 这种普通字段按逗号拼接,正好能看出两个值;但如果上游和应用对逗号的解释不同,合并可能改变协议语义。cookie 用分号连接,是 Cookie 请求头自身的格式,不是 Node 把所有字段统一拼起来。set-cookie 保留数组也很重要,合并成一行会失去每个值之间的边界。遇到重复字段,最好按具体字段的规范判断是否允许、怎样解释,再选读取接口。

content-type 的测试结果是两行输入、一个普通属性值:输出保留了第一项。若代码仅记录 req.headers,后续排查者不会从这份记录里知道第二行存在。需要审计输入时,rawHeaders 留下重复次数和次序;只想在业务逻辑里检查可接受的内容类型,则还需要明确处理出现多个值时的策略。将两个目的混在一个对象里,容易把解析器的便利行为误认为请求本身只有一个值。

读值前先定下需要的形状

一般路由取 req.headers['content-type'] 很方便,字段名大小写不再需要分支。需要每个重复值时,可读 req.headersDistinct['x-trace'];若要比对原始字段的出现顺序或大小写,则查 req.rawHeaders。这些属性用途不同,没有一个能替代另一个。

headersDistinct 把每个字段值都放进数组,避免合并规则造成的信息折叠。rawHeaders 更接近解析器收到的序列,既保留原始大小写,也保留重复字段出现的先后次序。它是交替排列的名称和值,适合核对输入,但不能把它当成已验证或可信任的数据。

若请求涉及签名或安全判断,先确认协议规则要求验证的是合并后的语义,还是输入序列。把 req.headers 序列化成 JSON 再拿去验签,会同时丢失原始大小写、顺序,以及某些字段的重复值;这不一定是 bug,但必须和签名端采用同一套规范化方式。headersDistinct 也不保留字段名的大小写,只有 rawHeaders 具备这项信息。

若服务前面还有反向代理,应用侧观察到的是代理转发后的请求,不能仅凭 rawHeaders 还原客户端到代理之间的原始报文。需要追查边界上的改写时,应在代理入口与 Node 服务分别记录或抓取允许留存的字段,并避免把 Cookie、授权头等敏感值写进普通日志。Node 的这些属性解决的是服务收到的头如何表示,不会替代链路上的审计。

协议边界需要按目标 Node 版本重跑,并为不同访问入口设置断言。若程序依赖原始请求头形状,测试应检查 rawHeaders,而不只检查整理后的 headers。

    🤞 分享