记一次nodejs源码bug分析
1. 问题
前几日,我在测试express
框架的时候,构造了一个测试样例死活过不来,即便调试到测试框架superagent
,依然不对。最终发现是nodejs
的bug,而且最新版本的nodejs
已经"修复"了,导致我中间饶了几圈都没发现是nodejs
的事,下面来重现问题流程。
2. 环境预备
- 安装一下
gnvm
地址,后面需要控制一下版本(windows10 需要用管理员权限的cmd或者powershell) - 安装git环境(主要要使用
curl
命令) - 摘抄如下代码
// 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.问题复现
- 安装nodejs (当前稳定版)版本
gnvm install 8.11.2
gnvm use 8.11.2
- 运行代码,
nodejs main.js
- 使用
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
=m
,entry
= [m, w]
则取出数据 field
= m
,value
= 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
尝试一下,最新版本已经没有问题了