← querystring.unescape() socket.setMulticastLoopback() 多播回环控制 →

Node.js response.getHeaderNames()响应头名称检索:从调试工具到中间件审计的工程用法

著
原创 2026-10-19 Node.js 已有人查阅

一、这个方法的定位:它是“当前已设置头”的快照,不是“将要发送的头”的保证

response.getHeaderNames()返回一个数组,包含当前在ServerResponse对象上通过setHeader设置的所有响应头名称,全部小写。官方版本标注为v7.7.0加入。

需要先厘清一个容易被示例掩盖的事实:它读取的是setHeader写入内部映射后的状态。如果你通过writeHead(200,{'Content-Type':'text/plain'})直接写入头部,这些头不会出现在getHeaderNames()的返回中,因为writeHead走的是另一条路径,头部值在调用时直接写入网络通道或暂存于待发送结构,不经过setHeader的内部映射缓存。Node.js文档对此有明确提示:如果调用writeHead且此前未调用setHeader,response.getHeader()不会得到预期结果;若需要渐进式填充头部并后续检索修改,应使用setHeader。

这意味着getHeaderNames()的适用场景被限定在:你(或你的中间件、框架)通过setHeader逐项设置响应头的流程中。如果你的项目大量使用res.writeHead(200,{...})一次性写头,这个方法能提供的信息有限。

个人经验:在Express项目中,res.setHeader是主流写法,getHeaderNames()的可用性较高。但在Fastify或原生writeHead较多的代码库中,直接依赖这个方法做审计会产生盲区。判断一个项目是否适合用它做中间件审计,先看setHeader的调用密度,而不是看方法文档本身。

二、与相邻方法的职责对比:不要用getHeaderNames做它不擅长的事

四个与响应头读取相关的方法,职责分得很开:

方法 返回内容 典型用途 关键限制
getHeaderNames() 字符串数组,小写名称 遍历已设置头的名称、审计、调试 不含writeHead写入的头
getHeader(name) 单个头的值 读取特定头的当前值 名称不区分大小写
getHeaders() 浅拷贝对象,键为小写名称 需要同时拿到名称和值 返回对象无原型,hasOwnProperty不可用
rawHeaders 请求/响应的原始名称-值扁平数组 排查大小写、重复头、原始顺序 在请求侧更常用,响应侧可读性差

如果你需要“拿到所有头的名称和值”,用getHeaders()一次获取,不要用getHeaderNames()遍历再逐个getHeader()。后者在头数量多时是多次属性查找,前者是一次性浅拷贝。getHeaderNames()的价值在于你只关心名称的场景,比如检查某个安全头是否被设置、统计自定义头的数量、在日志中记录头名列表。

三、大小写归一化的实际影响:小写返回是便利,也是

getHeaderNames()返回的名称全部小写。这与Node.js对HTTP头名称不区分大小写的内部规范化一致。在大多数场景中这是便利的:你用'Content-Type'设置,用'content-type'检索,不需要关心书写形式。

但在一个场景中,小写归一化会制造麻烦:当你需要将头名称用于大小写敏感的展示或比对时。例如,你在管理后台展示“这个响应设置了哪些自定义X-头”,用户期望看到X-Request-Id而非x-request-id。getHeaderNames()不会保留你设置时的原始大小写。如果你需要原始形态,必须从rawHeaders中提取,或者自己在设置时维护一份名称映射。

另一个需要留意的点:HTTP/2的Http2ServerResponse.getHeaderNames()同样返回小写名称。如果你的应用同时处理HTTP/1.1和HTTP/2请求,小写行为是一致的,不需要做分支处理。

四、单元测试与边界用例

以下测试覆盖getHeaderNames()的核心行为边界,可直接纳入项目测试。

const assert = require('node:assert');
const http = require('node:http');

// 用例 1:基本返回与顺序
const res1 = new http.ServerResponse({ method: 'GET' });
res1.setHeader('Content-Type', 'text/plain');
res1.setHeader('X-Request-Id', 'abc123');
assert.deepStrictEqual(res1.getHeaderNames(), ['content-type', 'x-request-id']);

// 用例 2:大小写归一化
const res2 = new http.ServerResponse({ method: 'GET' });
res2.setHeader('X-Custom-Header', 'v');
assert.deepStrictEqual(res2.getHeaderNames(), ['x-custom-header']);
// 用原始大小写检索,getHeader 仍可命中
assert.strictEqual(res2.getHeader('X-Custom-Header'), 'v');

// 用例 3:重复设置同名头,名称不重复
const res3 = new http.ServerResponse({ method: 'GET' });
res3.setHeader('X-A', '1');
res3.setHeader('x-a', '2');
assert.deepStrictEqual(res3.getHeaderNames(), ['x-a']);

// 用例 4:set-cookie 数组不影响名称列表
const res4 = new http.ServerResponse({ method: 'GET' });
res4.setHeader('Set-Cookie', ['a=1', 'b=2']);
assert.deepStrictEqual(res4.getHeaderNames(), ['set-cookie']);

// 用例 5:空状态
const res5 = new http.ServerResponse({ method: 'GET' });
assert.deepStrictEqual(res5.getHeaderNames(), []);

// 用例 6:removeHeader 之后名称消失
const res6 = new http.ServerResponse({ method: 'GET' });
res6.setHeader('X-Temp', 'v');
res6.removeHeader('X-Temp');
assert.deepStrictEqual(res6.getHeaderNames(), []);

关于headersSent之后的调用行为:getHeaderNames()在headersSent为true之后调用不会抛错,但它返回的是内部映射的残留状态,而非实际发送到客户端的头部。如果头已经通过网络发出,再修改setHeader也不会生效(Node.js会抛出ERR_HTTP_HEADERS_SENT)。因此在中间件中做头审计时,时机应放在业务处理器执行之前或res.end()调用之前,不要在finish事件之后依赖返回值做决策。

五、常见误区速查

误区一:用getHeaderNames()检查writeHead设置的头。如上所述,writeHead写入的头不进入setHeader的映射。如果你的审计中间件在路由处理器之前运行,而路由处理器用writeHead写头,审计时getHeaderNames()返回的是空数组或只包含中间件自己设置的头,看不到处理器将要写入的内容。

误区二:把返回的数组用于拼接res.setHeader的参数。数组中的名称是小写的,且只包含名称不包含值。要复制或迁移头部,用getHeaders()拿到键值对,而不是遍历getHeaderNames()再逐个getHeader。后者在set-cookie这类数组值上会丢失多值结构(getHeader('set-cookie')返回数组,但手动遍历时容易误用)。

误区三:在Express中间件中认为getHeaderNames()包含Express默认头。Express的res.send会在最终发送时补充部分头(如ETag、Content-Length),这些头在中间件执行阶段尚未设置,getHeaderNames()看不到。如果你在中间件中基于头名称做缓存策略判断,需要意识到这个时间差。

六、一个实用场景:中间件中的响应头合规审计

在需要确保所有API响应都携带特定安全头(如X-Content-Type-Options、Strict-Transport-Security)的项目中,getHeaderNames()可以作为一个轻量的审计钩子:

function headerAudit(req, res, next) {
  const originalEnd = res.end;
  res.end = function (...args) {
    const names = res.getHeaderNames();
    const required = ['x-content-type-options', 'strict-transport-security'];
    const missing = required.filter(n => !names.includes(n));
    if (missing.length > 0) {
      // 记录告警,不阻断响应
      console.warn(`[header-audit] ${req.method} ${req.path} missing: ${missing.join(', ')}`);
    }
    return originalEnd.apply(res, args);
  };
  next();
}

这个写法的局限在于它依赖setHeader的调用路径。如果某个路由直接调用res.writeHead写头,审计会误报缺失。因此在项目中,更稳妥的做法是把审计逻辑放在统一的响应封装函数中,或者在CI阶段用集成测试扫描实际发出的响应头,而不是在运行时依赖getHeaderNames()做强制校验。

七、版本差异说明(对应PHP版本差异要求的本主题映射)

getHeaderNames()是Node.jsAPI,不存在PHP版本对应关系。就Node.js自身版本而言,该方法的签名和语义自v7.7.0加入以来保持稳定。HTTP/2模块中的Http2ServerResponse.getHeaderNames()行为一致,返回小写名称数组。在Bun的Node.js兼容层中,ServerResponse.getHeaderNames同样被实现为返回小写唯一名称数组。跨运行时使用时,行为差异风险较低。

八、延伸阅读与参考文献

练习与思考

练习:写一个Express中间件,在每个响应完成时输出一行日志,格式为[headers]<method><path>-><n>headerssetviasetHeader,其中n通过getHeaderNames().length获取。要求:如果某个路由使用了res.writeHead写头,日志中的数量可能偏小,在注释中说明这一局限。

思考:你的项目中是否存在依赖getHeaderNames()返回值做业务判断的代码?如果某个中间件在res.end之后仍然读取这个数组,它拿到的值是否还能代表实际发出的头?在finish事件监听器中做头统计与在业务处理器执行前做头审计,两者的数据可信度差异在哪里?

← querystring.unescape() socket.setMulticastLoopback() 多播回环控制 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号