0%

koa之Content-Type与Content-Length

Content-Type

众所周知,koa可以方便地通过ctx.type=来设置响应头的Content-Type

但下面这段代码,当响应体ctx.body为object时,无论怎么设置ctx.type,收到的Content-Type都是application/json

1
2
3
4
ctx.type = 'text/html';
ctx.body = { type: 'json' };

// 得到的content-type依然为application/json

为什么设置被覆盖了,要通过一小段koa源码来解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// koa/lib/response.js

set body(val) {
const original = this._body;
this._body = val;

if (this.res.headersSent) return;

// no content
if (null == val) {
if (!statuses.empty[this.status]) this.status = 204;
this.remove('Content-Type');
this.remove('Content-Length');
this.remove('Transfer-Encoding');
return;
}

// set the status
if (!this._explicitStatus) this.status = 200;

// set the content-type only if not yet set
const setType = !this.header['content-type'];

// string
if ('string' == typeof val) {
if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
this.length = Buffer.byteLength(val);
return;
}

// buffer
if (Buffer.isBuffer(val)) {
if (setType) this.type = 'bin';
this.length = val.length;
return;
}

// stream
if ('function' == typeof val.pipe) {
onFinish(this.res, destroy.bind(null, val));
ensureErrorHandler(val, err => this.ctx.onerror(err));

// overwriting
if (null != original && original != val) this.remove('Content-Length');

if (setType) this.type = 'bin';
return;
}

// json
this.remove('Content-Length');
this.type = 'json';
}

熟悉koa源码的同学可能还记得,如果ctx.type未设置,会根据传给body的值类型赋予Content-Type默认值。

  • null/undefined:type什么的不存在的,即使有也会被删掉,并设置’No Content’ 204状态码
  • string:正则/^\s*</匹配,分情况设为html或text
  • Buffer:如果未设置type,那么会被改为bin(application/octet-stream)
  • Stream:同Buffer,当然逻辑上会多个绑定结束时destroy和异常处理,如果和旧body不同,还会删掉content-length
  • 以上都不是:即使设置过ctx.type,也会重新标记为json,并删除content-length

可想而知,既然已经过每一步判断,body的内容一般就是booleanobjectnumber之流,标记为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-typesmime-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// koa/lib/application.js
function respond(ctx) {
// ...

// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);

// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}

因此,一般我们对 json 类型的返回值显式更改 ctx.length 是没实际意义的,koa 的默认在 res.end 前强制把 length 覆盖。这个处理可以避免使用者错误地认为 body.length 就是响应数据的长度。

如果非要修改,去使用 ctx.res.writeHead 吧,如此一来,res.headersSent 内的处理就被会跳过了。

小结

koa的源码解析文章实在太多了,所以早先没有打算像express那样写一篇逐句分析,而且确实简单易读,恐怕没写完就太监了。但时间一久,很多细节就忘掉了,会遇到此类问题说明对其底层不够熟悉。就此写一篇笔记,发出来加深记忆。[真香.jpg]