[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"content-coding-is-a-sure-thing":3,"$f347kl9n7pdco5":21},{"id":4,"type":5,"slug":6,"title":7,"date":8,"category":9,"tags":10,"body_markdown":13,"permalink":14,"excerpt_src":14,"media_type":14,"media_title":14,"media_author":14,"media_url":14,"rating":14,"layout":14,"pv":15,"admin_only":15,"created_at":16,"updated_at":16,"deleted_at":14,"status":17,"html":18,"excerpt":19,"cover":20},188,"article","coding-is-a-sure-thing","写代码是一件与确定性为伍的事情","2020-09-19 13:39","web",[11,12],"stream","node.js","\n我们所处的世界充满了各种各样的不确定性。但有一件事是不存在不确定性的，即写代码。\n\n多年以前，在我刚入行不久的时候，有一位前辈和我说过“出现问题的时候，先怀疑是自己的原因，因为机器是不会出错的，错的永远是人。”这句话我记了很久，也时不时就会翻出来回想一番，也会冒出很多更细的想法：那机器不也是人造的？机器的程序不也是人写的？就一定是自己的原因，不能是别人的原因吗？但反复想了很多年，还是觉得这句话相当有道理，即使是别人的原因，那错的也是人而不是机器。\n\n这其实就是写代码时的确定性，我们写的代码会被怎么运行，是非常确定的。即便它要依赖更多的底层软硬件机制，但仍然是确定的，只是找出这个确定性的过程更加复杂而已。\n\n## 一个例子\n\n> 如果你看不懂例子，跳过就好。\n\n### 背景\n\n项目中需要上传下载文件，使用的是某云服务的存储服务。在下载的部分，为了方便，使用Node.js封装了一个下载方法，返回一个`Stream`，而这个`Stream`本质上是由http请求库request.js请求后返回的。最后由koa框架返回这个`Stream`给浏览器。\n\n请求下载 -> 下载方法 -> request.js请求云服务 -> 返回`Stream`\n\n代码大致如下：\n\n```javascript\nrouter.get('\u002Fapi\u002Fdownload-file', async (ctx) => {\n    ctx.body = Download.getPrivateStream(ctx.query.fileId);\n});\n```\n\n然而，同样的代码，在不同的项目下，表现却大不一样，A项目访问图片时是直接在浏览器中显示图片，B项目访问同样的图片却变成了下载。调试工具一查看，发现它们有不一样的HTTP Header返回：\n\n- A项目`Content-Type: image\u002Fpng`\n- B项目`Content-Type: application\u002Foctet-stream`\n\n\u003C!-- more -->\n\n### 解决\n\n经过初步排查，A B两个项目中都没有手工设置过这个Header值，可以基本确认这个差异并不是由于下载部分的写法造成的。\n\n虽然原因不是很明朗，但这个问题却很好解决：手工加一个设置`Content-Type`值的代码，一行代码就能解决。\n\n```javascript\nrouter.get('\u002Fapi\u002Fdownload-file', async (ctx) => {\n    ctx.type = mime.getType(fileExt);\n    ctx.body = Download.getPrivateStream(ctx.query.fileId);\n});\n```\n\n### 寻找确定性\n\n虽然上面的代码解决了这个应用场景下的问题，但却并没有找到真正的原因。也就是说，这里遗留了一段具有不确定性的代码。\n\n为了找到真正确定的原因，我在接下来的两天内花了一个晚上+一个上午的时间，从下载的封装到request.js的源码都一一做了排查，最终找到了原因。\n\nrequest.js在发现`response`（`Stream`）被`pipe`到一个新的`Stream`的时候，会尝试使用新`Stream`的`setHeader`方法，将源响应中的HTTP header都设置到新的`Stream`上。\n\n```javascript\nif (dest.headers && !dest.headersSent) {\n    if (response.caseless.has('content-type')) {\n        var ctname = response.caseless.has('content-type')\n        if (dest.setHeader) {\n            dest.setHeader(ctname, response.headers[ctname])\n        } else {\n            dest.headers[ctname] = response.headers[ctname]\n        }\n    }\n\n    if (response.caseless.has( 'content-length')) {\n        var clname = response.caseless.has( 'content-length')\n        if (dest.setHeader) {\n            dest.setHeader(clname, response.headers[clname])\n        } else {\n            dest.headers[clname] = response.headers[clname]\n        }\n    }\n}\n```\n\n然而调试到这里的时候会发现A B两个项目走到了不同的逻辑。B项目的新`Stream`（代码中的`dest`）并不存在`setHeader`方法。\n\n通过查看koa的源码，会发现这个`dest`其实就是`ctx.body`。按理说，`ctx.body`是一个http response stream，肯定是有`setHeader`方法的。那么，唯一的解释就是：有别的代码动过`ctx.body`了。\n\n最后经过一番排查，找到了一个万万想不到的事实：`koa-logger`会替换`ctx.body`\n\n```javascript\n\u002F\u002F calculate the length of a streaming response\n\u002F\u002F by intercepting the stream with a counter.\n\u002F\u002F only necessary if a content-length header is currently not set\nconst length = ctx.response.length\nconst body = ctx.body\nlet counter\nif (length == null && body && body.readable) {\n    ctx.body = body\n        .pipe(counter = Counter())\n        .on('error', ctx. onerror)\n}\n```\n\n原来，`koa-logger`为了记录响应体的大小，粗暴地将`ctx.body` `pipe`到了一个`Counter`实例上，并就此替换了`ctx.body`。刚好，A项目没有使用`koa-logger`，而B项目使用了。\n\n\n## 结\n\n因为出于对不确定性的不放心，所以尽管有现成的解决办法，但仍然没有放弃对它的追查。谁能想得到，最终的问题出在一个看起来人畜无害的代码库中呢？好在，花费了一番工夫，总算把一个不确定的事情变成了确定的事情。\n在这个例子中，可能我们会再慎重地评估这个代码库，即使不能第一时间进行替换和修复，也可以给出足够的文档，而这将为后续代码的稳定运行打下坚实基础。\n\n写代码是一件与确定性为伍的事情，如果你觉得你的代码有诸多不确定性，那只能说明一件事情，就是你又欠工夫了。\n\n> 2022-08更新：2年过去了，koa-logger仍然没有修复这个问题。\n>\n> [issue](https:\u002F\u002Fgithub.com\u002Fkoajs\u002Flogger\u002Fpull\u002F81) [Pull Request](https:\u002F\u002Fgithub.com\u002Fkoajs\u002Flogger\u002Fpull\u002F85)\n",null,0,"2026-08-28 04:37:17","published","\u003Cp>我们所处的世界充满了各种各样的不确定性。但有一件事是不存在不确定性的，即写代码。\u003C\u002Fp>\n\u003Cp>多年以前，在我刚入行不久的时候，有一位前辈和我说过“出现问题的时候，先怀疑是自己的原因，因为机器是不会出错的，错的永远是人。”这句话我记了很久，也时不时就会翻出来回想一番，也会冒出很多更细的想法：那机器不也是人造的？机器的程序不也是人写的？就一定是自己的原因，不能是别人的原因吗？但反复想了很多年，还是觉得这句话相当有道理，即使是别人的原因，那错的也是人而不是机器。\u003C\u002Fp>\n\u003Cp>这其实就是写代码时的确定性，我们写的代码会被怎么运行，是非常确定的。即便它要依赖更多的底层软硬件机制，但仍然是确定的，只是找出这个确定性的过程更加复杂而已。\u003C\u002Fp>\n\u003Ch2>一个例子\u003C\u002Fh2>\n\u003Cblockquote>\n\u003Cp>如果你看不懂例子，跳过就好。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3>背景\u003C\u002Fh3>\n\u003Cp>项目中需要上传下载文件，使用的是某云服务的存储服务。在下载的部分，为了方便，使用Node.js封装了一个下载方法，返回一个\u003Ccode>Stream\u003C\u002Fcode>，而这个\u003Ccode>Stream\u003C\u002Fcode>本质上是由http请求库request.js请求后返回的。最后由koa框架返回这个\u003Ccode>Stream\u003C\u002Fcode>给浏览器。\u003C\u002Fp>\n\u003Cp>请求下载 -&gt; 下载方法 -&gt; request.js请求云服务 -&gt; 返回\u003Ccode>Stream\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cp>代码大致如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>router.get('\u002Fapi\u002Fdownload-file', async (ctx) =&gt; {\n    ctx.body = Download.getPrivateStream(ctx.query.fileId);\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然而，同样的代码，在不同的项目下，表现却大不一样，A项目访问图片时是直接在浏览器中显示图片，B项目访问同样的图片却变成了下载。调试工具一查看，发现它们有不一样的HTTP Header返回：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>A项目\u003Ccode>Content-Type: image\u002Fpng\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>B项目\u003Ccode>Content-Type: application\u002Foctet-stream\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>解决\u003C\u002Fh3>\n\u003Cp>经过初步排查，A B两个项目中都没有手工设置过这个Header值，可以基本确认这个差异并不是由于下载部分的写法造成的。\u003C\u002Fp>\n\u003Cp>虽然原因不是很明朗，但这个问题却很好解决：手工加一个设置\u003Ccode>Content-Type\u003C\u002Fcode>值的代码，一行代码就能解决。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>router.get('\u002Fapi\u002Fdownload-file', async (ctx) =&gt; {\n    ctx.type = mime.getType(fileExt);\n    ctx.body = Download.getPrivateStream(ctx.query.fileId);\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>寻找确定性\u003C\u002Fh3>\n\u003Cp>虽然上面的代码解决了这个应用场景下的问题，但却并没有找到真正的原因。也就是说，这里遗留了一段具有不确定性的代码。\u003C\u002Fp>\n\u003Cp>为了找到真正确定的原因，我在接下来的两天内花了一个晚上+一个上午的时间，从下载的封装到request.js的源码都一一做了排查，最终找到了原因。\u003C\u002Fp>\n\u003Cp>request.js在发现\u003Ccode>response\u003C\u002Fcode>（\u003Ccode>Stream\u003C\u002Fcode>）被\u003Ccode>pipe\u003C\u002Fcode>到一个新的\u003Ccode>Stream\u003C\u002Fcode>的时候，会尝试使用新\u003Ccode>Stream\u003C\u002Fcode>的\u003Ccode>setHeader\u003C\u002Fcode>方法，将源响应中的HTTP header都设置到新的\u003Ccode>Stream\u003C\u002Fcode>上。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>if (dest.headers &amp;&amp; !dest.headersSent) {\n    if (response.caseless.has('content-type')) {\n        var ctname = response.caseless.has('content-type')\n        if (dest.setHeader) {\n            dest.setHeader(ctname, response.headers[ctname])\n        } else {\n            dest.headers[ctname] = response.headers[ctname]\n        }\n    }\n\n    if (response.caseless.has( 'content-length')) {\n        var clname = response.caseless.has( 'content-length')\n        if (dest.setHeader) {\n            dest.setHeader(clname, response.headers[clname])\n        } else {\n            dest.headers[clname] = response.headers[clname]\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然而调试到这里的时候会发现A B两个项目走到了不同的逻辑。B项目的新\u003Ccode>Stream\u003C\u002Fcode>（代码中的\u003Ccode>dest\u003C\u002Fcode>）并不存在\u003Ccode>setHeader\u003C\u002Fcode>方法。\u003C\u002Fp>\n\u003Cp>通过查看koa的源码，会发现这个\u003Ccode>dest\u003C\u002Fcode>其实就是\u003Ccode>ctx.body\u003C\u002Fcode>。按理说，\u003Ccode>ctx.body\u003C\u002Fcode>是一个http response stream，肯定是有\u003Ccode>setHeader\u003C\u002Fcode>方法的。那么，唯一的解释就是：有别的代码动过\u003Ccode>ctx.body\u003C\u002Fcode>了。\u003C\u002Fp>\n\u003Cp>最后经过一番排查，找到了一个万万想不到的事实：\u003Ccode>koa-logger\u003C\u002Fcode>会替换\u003Ccode>ctx.body\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F calculate the length of a streaming response\n\u002F\u002F by intercepting the stream with a counter.\n\u002F\u002F only necessary if a content-length header is currently not set\nconst length = ctx.response.length\nconst body = ctx.body\nlet counter\nif (length == null &amp;&amp; body &amp;&amp; body.readable) {\n    ctx.body = body\n        .pipe(counter = Counter())\n        .on('error', ctx. onerror)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>原来，\u003Ccode>koa-logger\u003C\u002Fcode>为了记录响应体的大小，粗暴地将\u003Ccode>ctx.body\u003C\u002Fcode> \u003Ccode>pipe\u003C\u002Fcode>到了一个\u003Ccode>Counter\u003C\u002Fcode>实例上，并就此替换了\u003Ccode>ctx.body\u003C\u002Fcode>。刚好，A项目没有使用\u003Ccode>koa-logger\u003C\u002Fcode>，而B项目使用了。\u003C\u002Fp>\n\u003Ch2>结\u003C\u002Fh2>\n\u003Cp>因为出于对不确定性的不放心，所以尽管有现成的解决办法，但仍然没有放弃对它的追查。谁能想得到，最终的问题出在一个看起来人畜无害的代码库中呢？好在，花费了一番工夫，总算把一个不确定的事情变成了确定的事情。\n在这个例子中，可能我们会再慎重地评估这个代码库，即使不能第一时间进行替换和修复，也可以给出足够的文档，而这将为后续代码的稳定运行打下坚实基础。\u003C\u002Fp>\n\u003Cp>写代码是一件与确定性为伍的事情，如果你觉得你的代码有诸多不确定性，那只能说明一件事情，就是你又欠工夫了。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>2022-08更新：2年过去了，koa-logger仍然没有修复这个问题。\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fkoajs\u002Flogger\u002Fpull\u002F81\">issue\u003C\u002Fa> \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fkoajs\u002Flogger\u002Fpull\u002F85\">Pull Request\u003C\u002Fa>\u003C\u002Fp>\n\u003C\u002Fblockquote>\n","\u003Cp>我们所处的世界充满了各种各样的不确定性。但有一件事是不存在不确定性的，即写代码。\u003C\u002Fp>\n\u003Cp>多年以前，在我刚入行不久的时候，有一位前辈和我说过“出现问题的时候，先怀疑是自己的原因，因为机器是不会出错的，错的永远是人。”这句话我记了很久，也时不时就会翻出来回想一番，也会冒出很多更细的想法：那机器不也是人造的？机器的程序不也是人写的？就一定是自己的原因，不能是别人的原因吗？但反复想了很多年，还是觉得这句话相当有道理，即使是别人的原因，那错的也是人而不是机器。\u003C\u002Fp>\n\u003Cp>这其实就是写代码时的确定性，我们写的代码会被怎么运行，是非常确定的。即便它要依赖更多的底层软硬件机制，但仍然是确定的，只是找出这个确定性的过程更加复杂而已。\u003C\u002Fp>\n\u003Ch2>一个例子\u003C\u002Fh2>\n\u003Cblockquote>\n\u003Cp>如果你看不懂例子，跳过就好。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3>背景\u003C\u002Fh3>\n\u003Cp>项目中需要上传下载文件，使用的是某云服务的存储服务。在下载的部分，为了方便，使用Node.js封装了一个下载方法，返回一个\u003Ccode>Stream\u003C\u002Fcode>，而这个\u003Ccode>Stream\u003C\u002Fcode>本质上是由http请求库request.js请求后返回的。最后由koa框架返回这个\u003Ccode>Stream\u003C\u002Fcode>给浏览器。\u003C\u002Fp>\n\u003Cp>请求下载 -&gt; 下载方法 -&gt; request.js请求云服务 -&gt; 返回\u003Ccode>Stream\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cp>代码大致如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>router.get('\u002Fapi\u002Fdownload-file', async (ctx) =&gt; {\n    ctx.body = Download.getPrivateStream(ctx.query.fileId);\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然而，同样的代码，在不同的项目下，表现却大不一样，A项目访问图片时是直接在浏览器中显示图片，B项目访问同样的图片却变成了下载。调试工具一查看，发现它们有不一样的HTTP Header返回：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>A项目\u003Ccode>Content-Type: image\u002Fpng\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>B项目\u003Ccode>Content-Type: application\u002Foctet-stream\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n","",{"total":15,"totalRoots":15,"comments":22,"pv":15},[]]