> ## Content Index
> Fetch the complete content index at: https://blog.hacking.pub/llms.txt
> Use this file to discover other available public pages before exploring further.

# 记一次nodejs源码bug分析
- URL: https://blog.hacking.pub/2018/05/18/ji-yi-ci-fen-xi-nodejs/
- Published: 2018-05-18T15:23:05.000Z
- Updated: 2020-04-19T10:00:02.000Z
- Author: chainhelen
- Tags: js, node, nodejs, express, 源码剖析

#### 1\. 问题

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

#### 2\. 环境预备

1. 安装一下`gnvm` [地址](https://github.com/Kenshin/gnvm?ref=blog.hacking.pub)，后面需要控制一下版本（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](https://github.com/nodejs/node/blob/v8.11.2/lib/%5Fhttp%5Foutgoing.js?ref=blog.hacking.pub#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](https://github.com/nodejs/node/blob/v8.11.2r/lib/%5Fhttp%5Fserver.js?ref=blog.hacking.pub#L202)

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

```

继续看一下`_http_outgoing.storeHeader()`，[\_http\_server#L307](https://github.com/nodejs/node/blob/v8.11.2/lib/%5Fhttp%5Foutgoing.js?ref=blog.hacking.pub#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](https://github.com/nodejs/node/blob/42a4a606457e01df51943c9c1e68ceaf9cafa0fe/lib/%5Fhttp%5Foutgoing.js?ref=blog.hacking.pub#L121)

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

```

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