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

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

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

HEAD 回了完整长度,响应体却是空的

2026-10-22 / 0 评论 / 3 阅读

HEAD 回了完整长度,响应体却是空的

把下载接口的 GET 换成 HEAD,想先确认资源大小,结果看到 Content-Length: 8,响应正文却是空的。光看头部像少传了数据;对 HEAD 来说,这个组合本来就可能是预期结果。

实验用 Node.js node:http 在 127.0.0.1 开一个临时端口,让同一个处理函数依次收到 GET /item 和 HEAD /item。服务端准备的正文是 小计 3,调用 Buffer.byteLength() 后设置 Content-Length,再把正文交给 res.end()。客户端则分别记录响应头和真正读到的字节数。没有浏览器、代理或外网参与。

同一份头,两个不同的正文长度

请求 状态码 Content-Length 客户端读到的正文
GET 200 8 8 字节
HEAD 200 8 0 字节

中文字符按 UTF-8 编码占多个字节。这里的 小计 3 是 8 字节,不是字符串里可见字符的个数。用 Buffer.byteLength() 计算后,GET 的头部和实际正文对得上;HEAD 保留同一个长度值,但客户端没有收到正文。

这是最容易误判的地方:不能把 HEAD 响应里有长度头,直接理解成响应体也跟着传了。要检查传输内容,应读响应数据流;只检查元数据时,长度头可以描述对应 GET 的正文长度。

脚本里的正文很小,长度差异只是为了让输出容易核对。换成大文件时,HEAD 能省掉响应正文的发送量,但不自动保证服务端跳过读取文件、计算长度或生成内容。若处理器每次都先把整个文件读进内存,再调用 res.end(),客户端少收了数据,服务端仍可能做了同样多的准备工作。实际收益要看服务端怎样生成这份响应。

服务端处理函数照样跑

脚本还在服务端记录了实际收到的方法:

GET /item request-bytes=0
HEAD /item request-bytes=0

这两条记录说明 HEAD 请求确实进入了同一个处理函数。Node.js 在这个 HEAD 响应里没有把正文交给客户端,但服务端的处理代码仍然执行。若处理函数里做了计数、写数据库或其他副作用,单纯换成 HEAD 并不会替应用跳过这些动作。这个实验没有测试任何业务框架的路由封装;框架是否自动复用 GET 处理器,需要单独确认。

完整脚本通过本机 HTTP 客户端各请求一次,保留响应头与实际读取到的字节数,随后关闭临时服务。客户端在响应流结束时汇总收到的 chunks,所以 responseBytes 不是按声明长度推算出来的;监听端口由系统分配,脚本退出前会关闭服务。实际输出是:

node v26.8.1
GET status=200 content-length=8 response-bytes=8
HEAD status=200 content-length=8 response-bytes=0
server requests:
GET /item request-bytes=0
HEAD /item request-bytes=0

什么时候值得加 HEAD

文件下载、静态资源或检查资源元数据时,HEAD 可以省掉响应正文的传输;它不能保证服务端什么都没做,也不能仅凭一次本机实验推断 CDN、代理和框架的行为。资源若在 GET 与 HEAD 之间变化,长度头还得由实际服务逻辑保证一致。

这份实验只确认了 Node.js 26.8.1 的 node:http 服务和客户端在本机的结果。本机浏览器和反向代理等真实链路没有覆盖。遇到「有 Content-Length 但 body 是空」的排查现场,先看请求方法,再对照实际读取的字节数;若目标是减少下载流量,HEAD 值得试,若目标是避免业务逻辑执行,则要在处理函数里明确分开路径。

可用 node tools/verify-head-response-node-http-20261022.mjs 重跑;实测日志保存在 tools/verify-head-response-node-http-20261022.log。我倾向于把 HEAD 当成只取响应元数据的入口,而不是保证服务端零成本的探针。

    🤞 分享