仓库源文站点原文


title: 记录 Got(Node.js) 代理 HTTP 请求的坑 date: 2021-01-27 19:05:25

tags: [Node.js,Stream,HTTP,proxy]

在过去,request 模块几乎是 Node.js 端的不二选择,可惜已被放弃维护。如今流行的模块虽然变多,但不意味着它们足够成熟,我还是倾向于专注 Node.js 端的那几个。

需求越简单,选择越不重要。不过相较而言,Got 的接口设计看起来更友好,并且它是做到支持 Connection Timeout 和 Read Timeout 的少数。

https://github.com/sindresorhus/got/#comparison 的 Advanced timeouts

实际用下来,还是遇到了坑,顺便扒了一眼 Got 的代码。

<!--more-->

native http?

先解释一下为什么不用原生 http 模块吧。

毕竟在 Node.js 提供代理服务是非常容易的事儿,普遍的优化无非是:

不用安装第三方依赖,基于原生 http 写上十来行代码即可完工,所以我自己很喜欢直接用 http 做一些工作。

但如果代理需求变得复杂,使用现成的轮子才能利于队友们(也包括自己)维护。

decompress

Got 默认对响应执行 decompress,对于代理而言毫无意义,需要关掉。

async _onResponseBase(response: IncomingMessageWithTimings): Promise<void> {
  const {options} = this;

  if (options.decompress) {
    response = decompressResponse(response);
  }
}

这在 decompress 的文档上有说明,但是一点儿都不醒目。

If this is disabled, a compressed response is returned as a Buffer. This may be useful if you want to handle decompression yourself or stream the raw compressed data.

最坑的是,我是因为遇到了 stream 的 bug (#issues/1279) 才注意到这个选项。在开启 decompress 的情况下,使响应值的 content-length 是错误的,该 bug 会导致返回的结果不完整。

accept-encoding

关闭 decompress 之后,响应开始变慢到肉眼可见得慢。

原因如下:

async _makeRequest(): Promise<void> {
  const {options} = this;
  const {headers} = options;

  if (options.decompress && is.undefined(headers['accept-encoding'])) {
    headers['accept-encoding'] = supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate';
  }
}

accept-encoding 只在 decompress 为 true 的时候设置,否则无法享用 gzip 等压缩带来的优化。

solution

const got = require('got');
const HttpAgent = require('agentkeepalive');
const HttpsAgent = HttpAgent.HttpsAgent;

const sec = 1000;
const min = 60 * sec;

const supportsBrotli = process.versions.brotli;

const request = got.extend({
  agent: {
    http: new HttpAgent(),
    https: new HttpsAgent(),
  },
  // content-length is not set correctly when streaming with decompress
  // #issues/1279
  decompress: false,
  // accept-encoding won't be set without decompress
  headers: {
    'accept-encoding': supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate',
  },
  timeout: {
    connect: 3 * sec,
    socket: 2 * min,
  },
});

这大概率不是最终版,立一个 flag,如果再遇到问题,就自己从头写一个专用于代理场景的 http 库。