记一次nodejs源码bug分析

1. 问题

前几日,我在测试express框架的时候,构造了一个测试样例死活过不来,即便调试到测试框架superagent ,依然不对。最终发现是nodejs的bug,而且最新版本的nodejs已经"修复"了,导致我中间饶了几圈都没发现是nodejs的事,下面来重现问题流程。

2. 环境预备

  1. 安装一下gnvm 地址,后面需要控制一下版本(windows10 需要用管理员权限的cmd或者powershell)
  2. 安装git环境(主要要使用curl命令)
  3. 摘抄如下代码
// main.js
var http = require('http')

var tmpObject = Object  
tmpObject.prototype['love'] = 'express'  
var server = http.createServer(function (_, res) {  
  res.setHeader("m", "w")
  res.end()
})
server.listen(3010)  

3.问题复现

  1. 安装nodejs (当前稳定版)版本
gnvm install 8.11.2  
gnvm use 8.11.2  
  1. 运行代码,nodejs main.js
  2. 使用curl -i 127.0.0.1:3010命令,得到如下
$ curl -i 127.0.0.1:3010
HTTP/1.1 200 OK  
m: w  
e: x  
Date: Fri, 18 May 2018 14:06:47 GMT  
Connection: keep-alive  
Content-Length: 0  

能理解有一个头 m: w,但是e: x是从哪来的?
明明奇怪的改动只是Object.prototype.love='express'

4.再次测试

修改一下main.js的代码,注释掉res.setHeader("m", "w")试试看

// main.js
var http = require('http')

var tmpObject = Object  
tmpObject.prototype['love'] = 'express'  
var server = http.createServer(function (_, res) {  
  // res.setHeader("m", "w")
  res.end()
})
server.listen(3010)

$ curl -i 127.0.0.1:3010
HTTP/1.1 200 OK  
Date: Fri, 18 May 2018 14:30:01 GMT  
Connection: keep-alive  
Content-Length: 0  


竟然没有了

5.解释

翻阅v8.11.2代码
_http_outgoing.js#L497

OutgoingMessage.prototype.setHeader = function setHeader(name, value) {  
  ...
  if (!this[outHeadersKey])
    this[outHeadersKey] = {};

  const key = name.toLowerCase();
  this[outHeadersKey][key] = [name, value];
  ...
};

那么3测试里面代码执行的时候,保存header的数据是这样的

this[outHeaderKey] = {  
    "m": ["m", "w"]
}


另外注意变量初始化this[outHeadersKey] = {}
那么this[outHeaderKey]的原型链指向Object.prototype

有了上面的认知,来看下res.end()做了哪些事,写一下调用链
_http_outgoing.end() => _http_server._implicitHeader() => _http_server.writeHead() => _http_outgoing._storeHeader()

看一下_http_server.writeHead()_http_server#L202

    headers = this[outHeadersKey];
    this._storeHeader(statusLine, headers);

继续看一下_http_outgoing.storeHeader()_http_server#L307

if (headers === this[outHeadersKey]) {  
    for (key in headers) {
      var entry = headers[key];
      field = entry[0];
      value = entry[1];
      ...
 }

1.当上述for in遍历到自定义res.setHeader("m", "w") 中的 "m":["m": "w"]
key=mentry = [m, w]
则取出数据 field = mvalue = w,没毛病

2.但当for in遍历到原型链的时候,key = 'love'entry = 'express'
那么field = entry[0] = 'e'value = entry[1] = 'x'
故而响应头中的 e:x 就是这么来的

6.小结

本质上是for in遍历到原型链,加上nodejs保存 outHeadersKey 的"奇怪"数组方式

才会导致发包过程中出现了一个难以理解的header

另外,对于for in来说,项目中通常采用hasOwnProperty来规避问题,但是新版本nodejs不是这样做的,下面是最新的nodejs这块代码
_http_outgoing.js#L121

 const headers = this[outHeadersKey] = Object.create(null);

Object.create(null)会把创建出来的对象__proto__ 指向 null
for in 就不会遍历到了,可以使用gnvm use v10.1.0尝试一下,最新版本已经没有问题了