Content-Type
众所周知,koa可以方便地通过ctx.type=
来设置响应头的Content-Type
。
但下面这段代码,当响应体ctx.body为object
时,无论怎么设置ctx.type,收到的Content-Type都是application/json
。
1 | ctx.type = 'text/html'; |
为什么设置被覆盖了,要通过一小段koa源码来解释。
1 | // koa/lib/response.js |
熟悉koa源码的同学可能还记得,如果ctx.type未设置,会根据传给body的值类型赋予Content-Type
默认值。
null/undefined
:type什么的不存在的,即使有也会被删掉,并设置’No Content’ 204状态码string
:正则/^\s*</
匹配,分情况设为html或textBuffer
:如果未设置type,那么会被改为bin
(application/octet-stream)Stream
:同Buffer,当然逻辑上会多个绑定结束时destroy和异常处理,如果和旧body不同,还会删掉content-length- 以上都不是:即使设置过
ctx.type
,也会重新标记为json,并删除content-length
可想而知,既然已经过每一步判断,body的内容一般就是boolean
、object
或number
之流,标记为json
合情合理。当然没忘symbol
,它是无法被JSON序列化的,会抛出TypeError。
综上,如果接口返回值满足上述几个json类型,又想更改响应头的content-type,最简单的方法其实是ctx.type=
放在ctx.body=
之后,以重新覆盖响应头的内容。但一些路由中间件封装的时候没有考虑这层面,把接口返回值作为ctx.body,添加这种在body后设置type的操作可能会遇到困难。
另一种常规方法是调整数据格式后再对ctx.body赋值,例如对object进行JSON.stringify处理后,之前设置的content-type才不会被覆盖为application/json
。
ctx.type的设置支持各类缩略词,每次set前都会通过
mime-types
和mime-db
依赖匹配完整名称,并挂上charset。
除了设置,还需要注意的点是ctx.type的getter会过滤掉
charset
部分,因此ctx.type的setter和getter不是完全对等的。如果想获取之前设置过的完整信息,需要通过ctx.res.getHeader('Content-Type')
到头部获取。
Wait,好像还漏了什么?
content-type既然在最后给定为json,为什么执行了一个this.remove('Content-Length')
?且听下面分解。
Content-Length
在set body
的string和Buffer步骤,都会主动判断字节长度(不是字符,所以string用Buffer.byteLength判断),并对this.length即content-length赋值。而json和stream则相反,不但没有设置length,反而把设置过的删掉了。
简单分析一下,stream步骤的删除不难理解,二进制数据流只有传输完毕才能计算长度,不需要从响应头判断length。另一方面,content-length是允许胡乱设置的,koa为了避免它被设置一个错误的值,所以才有了的重新赋值与删除。甚至在异常捕获中也加上了这个fix:Content-Length not reset if error is thrown after body is set。
那么json返回值的length被删掉之后,它是从哪里重新被设置呢?
最初我错想为交给了node底层处理,而且确实在http模块中有对content-length的设置,但它只有在完全未指定headers时才会添加。[https://github.com/nodejs/node/blob/master/lib/_http_outgoing.js]
实际对json类型的值判断字节长度非常容易,JSON.stringify加Buffer.byteLength即可。目录内搜索一下对this.length或ctx.length的赋值,果然,在响应的最终res.end()之前看到了该处理。
1 | // koa/lib/application.js |
因此,一般我们对 json 类型的返回值显式更改 ctx.length
是没实际意义的,koa 的默认在 res.end 前强制把 length 覆盖。这个处理可以避免使用者错误地认为 body.length 就是响应数据的长度。
如果非要修改,去使用 ctx.res.writeHead 吧,如此一来,res.headersSent 内的处理就被会跳过了。
小结
koa的源码解析文章实在太多了,所以早先没有打算像express那样写一篇逐句分析,而且确实简单易读,恐怕没写完就太监了。但时间一久,很多细节就忘掉了,会遇到此类问题说明对其底层不够熟悉。就此写一篇笔记,发出来加深记忆。[真香.jpg]