仓库源文站点原文


title: koa之Content-Type与Content-Length date: 2018-10-18 22:16:05 tags: [Node.js,JavaScript,Koa] categories: Node.js

description: 透过ctx.body setter的执行过程,分析koa如何设置响应头content-type和content-length,以及一些注意事项。

Content-Type

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

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

ctx.type = 'text/html';
ctx.body = { type: 'json' };

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

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

// 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默认值。

可想而知,既然已经过每一步判断,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()之前看到了该处理。

// 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]