eggjs中HttpClient网络及Curl的使用

# HttpClient

封装后跟requet库操作类似

# 通过 app 使用 HttpClient

框架在应用初始化的时候,会自动将 HttpClient (opens new window) 初始化到 app.httpclient。 同时增加了一个 app.curl(url, options) 方法,它等价于 app.httpclient.request(url, options)

这样就可以非常方便地使用 app.curl 方法完成一次 HTTP 请求。

# 通过 ctx 使用 HttpClient

框架在 Context 中同样提供了 ctx.curl(url, options)ctx.httpclient,保持跟 app 下的使用体验一致。 这样就可以在有 Context 的地方(如在 controller 中)非常方便地使用 ctx.curl() 方法完成一次 HTTP 请求。

const result = await app.curl('https://registry.npm.taobao.org/egg/latest', {
      dataType: 'json',
    }); 
const result = await ctx.curl('https://registry.npm.taobao.org/egg/latest', {
      // 自动解析 JSON response
      dataType: 'json',
      // 3 秒超时
      timeout: 3000,
    });
 ctx.body = {
     status: result.status,
     headers: result.headers,
     package: result.data,
 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 基本 HTTP 请求(封装后跟requet库操作类似)

HttpClient 的默认 method 会设置为 GET

  • 返回值 result 会包含 3 个属性:status, headersdata

    status: 响应状态码,如 200, 302, 404, 500 等等

    • headers: 响应头,类似 { 'content-type': 'text/html', ... }
    • data: 响应 body,默认 HttpClient 不会做任何处理,会直接返回 Buffer 类型数据。 一旦设置了 options.dataType,HttpClient 将会根据此参数对 data 进行相应的处理。

完整的请求参数 options 和返回值 result 的说明请看下文的 options 参数详解 (opens new window) 章节。

以下例子都会在 controller 代码中对 https://httpbin.org 发起请求来完成。

#get 读取数据几乎都是使用 GET 请求,它是 HTTP 世界最常见的一种,也是最广泛的一种,它的请求参数也是最容易构造的。
const result = await ctx.curl('https://httpbin.org/get?foo=bar');
#post 创建数据的场景一般来说都会使用 POST 请求,它相对于 GET 来说多了请求 body 这个参数。
const result = await ctx.curl('https://httpbin.org/post', {
    method: 'POST', // 必须指定 method
    contentType: 'json',// 通过 contentType 告诉 HttpClient 以 JSON 格式发送
    dataType: 'json',// 明确告诉 HttpClient 以 JSON 格式处理返回的响应 body
    data: {
        hello: 'world',
        now: Date.now(),
    },
});
#put PUTPOST 类似,它更加适合更新数据和替换数据的语义。 除了 method 参数需要设置为 PUT,其他参数几乎跟 POST 一模一样。
const result = await ctx.curl('https://httpbin.org/put', {
    method: 'PUT',
    contentType: 'json',
    dataType: 'json',
    data: {
        update: 'foo bar',
    },
});
#delete  删除数据会选择 DELETE 请求,它通常可以不需要加请求 body,但是 HttpClient 不会限制。
 const result = await ctx.curl('https://httpbin.org/delete', {
     method: 'DELETE',
     dataType: 'json',
 });

ctx.body = result.data;
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

request request-promise 库使用示例:

#request request-promise 库使用示例
const rp = require('request-promise');
  var options = {
    method: 'POST',
    uri: `https://devpython.iwifi.com:8082/Meet/face/V1804261328.U.JSON`,//设备状态;
    // headers: {
    //   /* 'content-type': 'application/x-www-form-urlencoded' */ // Is set automatically
    // },
    json: true, // Automatically stringifies the body to JSON
    // body: reqData,//POST data to a JSON REST API
    // form: reqData,//POST like HTML forms do
    formData: reqData,//if you want to include a file upload then use options.formData:
    headers: {
      /* 'content-type': 'multipart/form-data' */ // Is set automatically
    }
  };
rp(options)
    .then(function(parsedBody) {
    // POST succeeded...
    console.log(parsedBody);
})
    .catch(function(err) {
    // POST failed...
    console.log(err);
});
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

# 高级 HTTP 请求

在真实的应用场景下,还是会包含一些较为复杂的 HTTP 请求。

# Form 表单提交

面向浏览器设计的 Form 表单(不包含文件)提交接口,通常都要求以 content-type: application/x-www-form-urlencoded 的格式提交请求数据。

// 不需要设置 contentType,HttpClient 会默认以 application/x-www-form-urlencoded 格式发送请求`` **ctx.body = result.data.form;**``**通过 \**querystring.stringify(data)`** 处理,并设置为 body 发送。 **```以 Multipart 方式上传文件

当一个 Form 表单提交包含文件的时候,请求数据格式就必须以 multipart/form-data (opens new window) 进行提交了。

urllib (opens new window) 内置了 formstream (opens new window) 模块来帮助我们生成可以被消费的 form 对象。

ctx.body = result.data.files;

# 以 Stream 方式上传文件

其实,在 Node.js 的世界里面,Stream 才是主流。 如果服务端支持流式上传,最友好的方式还是直接发送 Stream。 Stream 实际会以 Transfer-Encoding: chunked 传输编码格式发送,这个转换是 HTTP (opens new window) 模块自动实现的。

#Form 表单提交
const result = await ctx.curl('https://httpbin.org/post', {
      // 必须指定 method,支持 POST,PUT 和 DELETE
      method: 'POST',
      //contentType: 'json',// 不需要设置 contentType,HttpClient 会默认以 application/x-www-form-urlencoded 格式发送请求
      dataType: 'json', // 明确告诉 HttpClient 以 JSON 格式处理响应 body
      data: {
        now: Date.now(),
        foo: 'bar',
      },
      
    });
    ctx.body = result.data.form;
    // 响应最终会是类似以下的结果:
    // {
    //   "foo": "bar",
    //   "now": "1483864184348"
    // }
#以 Multipart 方式上传文件
const result = await ctx.curl('https://httpbin.org/post', {
      method: 'POST',
      dataType: 'json',
      data: {
        foo: 'bar',
      },
      // 单文件上传
      files: __filename,
      // 多文件上传
      // files: {
      //   file1: __filename,
      //   file2: fs.createReadStream(__filename),
      //   file3: Buffer.from('mock file content'),
      // },
    });
    ctx.body = result.data.files;
    // 响应最终会是类似以下的结果:
    // {
    //   "file": "'use strict';\n\nconst For...."
    // }

#以 Stream 方式上传文件
const fs = require('fs');
const FormStream = require('formstream');
  async uploadByStream() {
    const ctx = this.ctx;
    // 上传当前文件本身用于测试
    const fileStream = fs.createReadStream(__filename);
    // httpbin.org 不支持 stream 模式,使用本地 stream 接口代替
    const url = `${ctx.protocol}://${ctx.host}/stream`;
    const result = await ctx.curl(url, {
      method: 'POST',// 必须指定 method,支持 POST,PUT
      stream: fileStream,// 以 stream 模式提交
    });
    ctx.status = result.status;
    ctx.set(result.headers);
    ctx.body = result.data;
    // 响应最终会是类似以下的结果:
    // {"streamSize":574}
  }
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
54
55
56
57
58
59

# options 参数详解

# HttpClient 默认全局配置

应用可以通过 config/config.default.js 覆盖此配置。

// config/config.default.js
exports.httpclient = {
  // 是否开启本地 DNS 缓存,默认关闭,开启后有两个特性
  // 1. 所有的 DNS 查询都会默认优先使用缓存的,即使 DNS 查询错误也不影响应用
  // 2. 对同一个域名,在 dnsCacheLookupInterval 的间隔内(默认 10s)只会查询一次
  enableDNSCache: false,
  // 对同一个域名进行 DNS 查询的最小间隔时间
  dnsCacheLookupInterval: 10000,
  // DNS 同时缓存的最大域名数量,默认 1000
  dnsCacheMaxLength: 1000,

  request: {
    // 默认 request 超时时间
    timeout: 3000,
  },

  httpAgent: {
    // 默认开启 http KeepAlive 功能
    keepAlive: true,
    // 空闲的 KeepAlive socket 最长可以存活 4 秒
    freeSocketTimeout: 4000,
    // 当 socket 超过 30 秒都没有任何活动,就会被当作超时处理掉
    timeout: 30000,
    // 允许创建的最大 socket 数
    maxSockets: Number.MAX_SAFE_INTEGER,
    // 最大空闲 socket 数
    maxFreeSockets: 256,
  },

  httpsAgent: {
    // 默认开启 https KeepAlive 功能
    keepAlive: true,
    // 空闲的 KeepAlive socket 最长可以存活 4 秒
    freeSocketTimeout: 4000,
    // 当 socket 超过 30 秒都没有任何活动,就会被当作超时处理掉
    timeout: 30000,
    // 允许创建的最大 socket 数
    maxSockets: Number.MAX_SAFE_INTEGER,
    // 最大空闲 socket 数
    maxFreeSockets: 256,
  },
};
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

# data: Object

需要发送的请求数据,根据 method 自动选择正确的数据处理方式。

  • GET,HEAD:通过 querystring.stringify(data) 处理后拼接到 url 的 query 参数上。

  • POST,PUT 和 DELETE 等:需要根据 contentType 做进一步判断处理。

    • contentType = json:通过 JSON.stringify(data) 处理,并设置为 body 发送。
    • 其他:通过 querystring.stringify(data) 处理,并设置为 body 发送。

# dataAsQueryString: Boolean

如果设置了 dataAsQueryString=true,那么即使在 POST 情况下, 也会强制将 options.dataquerystring.stringify 处理之后拼接到 url 的 query 参数上。

可以很好地解决以 stream 发送数据,且额外的请求参数以 url query 形式传递的应用场景:

一般来说都是 access token 之类的权限验证参数

ctx.curl(url, {
  method: 'POST',
  dataAsQueryString: true,
  data: { // 一般来说都是 access token 之类的权限验证参数
    accessToken: 'some access token value',
  },
  stream: myFileStream,
});
1
2
3
4
5
6
7
8

# content: String|Buffer

*发送请求正文,如果设置了此参数,那么会直接忽略 data 参数。

# files: Mixed

文件上传,支持格式: String | ReadStream | Buffer | Array | Object

ctx.curl(url, {
  method: 'POST',
  files: '/path/to/read',
  data: {
    foo: 'other fields',
  },
});
1
2
3
4
5
6
7

多文件上传:

ctx.curl(url, {
  method: 'POST',
  files: {
    file1: '/path/to/read',
    file2: fs.createReadStream(__filename),
    file3: Buffer.from('mock file content'),
  },
  data: {
    foo: 'other fields',
  },
});
1
2
3
4
5
6
7
8
9
10
11

# stream: ReadStream

设置发送请求正文的可读数据流,默认是 null。 一旦设置了此参数,HttpClient 将会忽略 datacontent

ctx.curl(url, {
  method: 'POST',
  stream: fs.createReadStream('/path/to/read'),
});
1
2
3
4

# writeStream: WriteStream

设置接受响应数据的可写数据流,默认是 null。 一旦设置此参数,那么返回值 result.data 将会被设置为 null, 因为数据已经全部写入到 writeStream 中了。

ctx.curl(url, {
  writeStream: fs.createWriteStream('/path/to/store'),
});
1
2
3

# consumeWriteStream: Boolean

是否等待 writeStream 完全写完才算响应全部接收完毕,默认是 true。 此参数不建议修改默认值,除非我们明确知道它的副作用是可接受的, 否则很可能会导致 writeStream 数据不完整。

# contentType: String

设置请求数据格式,默认是 undefined,HttpClient 会自动根据 datacontent 参数自动设置。

data 是 object 的时候默认设置的是 form。支持 json 格式。

如需要以 JSON 格式发送 data

# dataType: String

设置响应数据格式,默认不对响应数据做任何处理,直接返回原始的 buffer 格式数据。 支持 textjson 两种格式。

注意:设置成 json 时,如果响应数据解析失败会抛 JSONResponseFormatError 异常。

const jsonResult = await ctx.curl(url, {
  dataType: 'json',
});
console.log(jsonResult.data);

const htmlResult = await ctx.curl(url, {
  dataType: 'text',
});
console.log(htmlResult.data);
1
2
3
4
5
6
7
8
9

# fixJSONCtlChars: Boolean

是否自动过滤响应数据中的特殊控制字符 (U+0000 ~ U+001F),默认是 false。 通常一些 CGI 系统返回的 JSON 数据会包含这些特殊控制字符,通过此参数可以自动过滤掉它们。

ctx.curl(url, {
  fixJSONCtlChars: true,
  dataType: 'json',
});
1
2
3
4

# timeout: Number|Array

请求超时时间,默认是 [ 5000, 5000 ],即创建连接超时是 5 秒,接收响应超时是 5 秒。

ctx.curl(url, {
  timeout: 3000,// 创建连接超时 3 秒,接收响应超时 3 秒
  timeout: [ 1000, 30000 ], // 创建连接超时 1 秒,接收响应超时 30 秒,用于响应比较大的场景
})
1
2
3
4

# agent: HttpAgent

允许通过此参数覆盖默认的 HttpAgent,如果你不想开启 KeepAlive,可以设置此参数为 false

# httpsAgent: HttpsAgent

允许通过此参数覆盖默认的 HttpsAgent,如果你不想开启 KeepAlive,可以设置此参数为 false

# auth: String

简单登录授权(Basic Authentication)参数,将以明文方式将登录信息以 Authorization 请求头发送出去。

# digestAuth: String

摘要登录授权(Digest Authentication)参数,设置此参数会自动对 401 响应尝试生成 Authorization 请求头, 尝试以授权方式请求一次。

ctx.curl(url, {
  auth: 'foo:bar',// 参数必须按照 `user:password` 格式设置
  digestAuth: 'foo:bar',// 参数必须按照 `user:password` 格式设置
});
1
2
3
4

# followRedirect: Boolean

是否自动跟进 3xx 的跳转响应,默认是 false

# maxRedirects: Number

设置最大自动跳转次数,避免循环跳转无法终止,默认是 10 次。 此参数不宜设置过大,它只在 followRedirect=true 情况下才会生效。

ctx.curl(url, {
  followRedirect: true,
  maxRedirects: 5,// 最大只允许自动跳转 5 次。
});
1
2
3
4

# formatRedirectUrl: Function(from, to)

允许我们通过 formatRedirectUrl 自定义实现 302、301 等跳转 url 拼接, 默认是 url.resolve(from, to)。 // 例如可在这里修正跳转不正确的 url

ctx.curl(url, {
  formatRedirectUrl: (from, to) => {
    // 例如可在这里修正跳转不正确的 url
    if (to === '//foo/') {
      to = '/foo';
    }
    return url.resolve(from, to);
  },
});
1
2
3
4
5
6
7
8
9

# beforeRequest: Function(options)

HttpClient 在请求正式发送之前,会尝试调用 beforeRequest 钩子,允许我们在这里对请求参数做最后一次修改。*我们可以设置全局请求 id,方便日志跟踪

 beforeRequest: options => {
    options.headers['x-request-id'] = uuid.v1(); // 例如我们可以设置全局请求 id,方便日志跟踪
  },
1
2
3

# streaming: Boolean

是否直接返回响应流,默认为 false。 开启 streaming 之后,HttpClient 会在拿到响应对象 res 之后马上返回, 此时 result.headersresult.status 已经可以读取到,只是没有读取 data 数据而已。

注意:如果 res 不是直接传递给 body,那么我们必须消费这个 stream,并且要做好 error 事件处理。

const result = await ctx.curl(url, {
  streaming: true,
  gzip: true,
});

console.log(result.status, result.data);
// result.res 是一个 ReadStream 对象
ctx.body = result.res;
1
2
3
4
5
6
7
8

# gzip: Boolean

是否支持 gzip 响应格式,默认为 false。 开启 gzip 之后,HttpClient 将自动设置 Accept-Encoding: gzip 请求头, 并且会自动解压带 Content-Encoding: gzip 响应头的数据。

# timing: Boolean

是否开启请求各阶段的时间测量,默认为 false。 开启 timing 之后,可以通过 result.res.timing 拿到这次 HTTP 请求各阶段的时间测量值(单位是毫秒), 通过这些测量值,我们可以非常方便地定位到这次请求最慢的环境发生在那个阶段,效果如同 Chrome network timing 的作用。

timing 各阶段测量值解析:

  • queuing:分配 socket 耗时
  • dnslookup:DNS 查询耗时
  • connected:socket 三次握手连接成功耗时
  • requestSent:请求数据完整发送完毕耗时
  • waiting:收到第一个字节的响应数据耗时
  • contentDownload:全部响应数据接收完毕耗时
const result = await ctx.curl(url, {
  timing: true,
});
console.log(result.res.timing);
// {
//   "queuing":29,
//   "dnslookup":37,
//   "connected":370,
//   "requestSent":1001,
//   "waiting":1833,
//   "contentDownload":3416
// }
1
2
3
4
5
6
7
8
9
10
11
12

ca,rejectUnauthorized,pfx,key,cert,passphrase,ciphers,secureProtocol

这几个都是透传给 HTTPS (opens new window) 模块的参数,具体请查看 https.request(options, callback) (opens new window)

# 调试辅助

如果你需要对 HttpClient 的请求进行抓包调试,可以添加以下配置到 config.local.js:process.env.http_proxy

// config.local.js
module.exports = () => {
  const config = {};
  // add http_proxy to httpclient
  if (process.env.http_proxy) {
    config.httpclient = {
      request: {
        enableProxy: true,
        rejectUnauthorized: false,
        proxy: process.env.http_proxy,
      },
    };
  }
  return config;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

然后启动你的抓包工具,如 charles (opens new window)fiddler (opens new window)。最后通过以下指令启动应用:

然后就可以正常操作了,所有经过 HttpClient 的请求,都可以你的抓包工具中查看到

http_proxy=http://127.0.0.1:8888 npm run dev
1

# 常见错误

# 创建连接超时

  • 异常名称:ConnectionTimeoutError
  • 出现场景:通常是 DNS 查询比较慢,或者客户端与服务端之间的网络速度比较慢导致的。
  • 排查建议:请适当增大 timeout 参数。

# 服务响应超时

  • 异常名称:ResponseTimeoutError
  • 出现场景:通常是客户端与服务端之间网络速度比较慢,并且响应数据比较大的情况下会发生。
  • 排查建议:请适当增大 timeout 参数。

# 服务主动断开连接

  • 异常名称:ResponseError, code: ECONNRESET
  • 出现场景:通常是服务端主动断开 socket 连接,导致 HTTP 请求链路异常。
  • 排查建议:请检查当时服务端是否发生网络异常。

# 服务不可达

  • 异常名称:RequestError, code: ECONNREFUSED, status: -1
  • 出现场景:通常是因为请求的 url 所属 IP 或者端口无法连接成功。
  • 排查建议:请确保 IP 或者端口设置正确。

# 域名不存在

  • 异常名称:RequestError, code: ENOTFOUND, status: -1
  • 出现场景:通常是因为请求的 url 所在的域名无法通过 DNS 解析成功。
  • 排查建议:请确保域名存在,也需要排查一下 DNS 服务是否配置正确。

# JSON 响应数据格式错误

  • 异常名称:JSONResponseFormatError
  • 出现场景:设置了 dataType=json 并且响应数据不符合 JSON 格式,就会抛出此异常。
  • 排查建议:确保服务端无论在什么情况下都要正确返回 JSON 格式的数据。

# 全局 requestresponse 事件

在企业应用场景,常常会有统一 tracer 日志的需求。 为了方便在 app 层面统一监听 HttpClient 的请求和响应,我们约定了全局 requestresponse 来暴露这两个事件。

# request 事件:发生在网络操作发生之前

请求发送之前,会触发一个 request 事件,允许对请求做拦截。

# response 事件:发生在网络操作结束之后

请求结束之后会触发一个 response 事件,这样外部就可以订阅这个事件打印日志。

init options
    |
    V
emit `request` event
    |
    V
send request and receive response
    |
    V
emit `response` event
    |
    V
   end
   
app.httpclient.on('request', req => {
  req.url //请求 url
  req.ctx //是发起这次请求的当前上下文
  // 可以在这里设置一些 trace headers,方便全链路跟踪
});

app.httpclient.on('response', result => {
  result.res.status
  result.ctx //是发起这次请求的当前上下文
  result.req //对应的 req 对象,即 request 事件里面那个 req
});
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

# 示例

参照对比:

curl请求模拟及eggjs处理对应示例

示例:

https://github.com/eggjs/examples/blob/master/httpclient/app/controller/httpclient.js

module.exports = app => {
  app.router.get('/', 'httpclient.home');
  app.router.get('/get', 'httpclient.get');
  app.router.get('/post', 'httpclient.post');
  app.router.get('/put', 'httpclient.put');
  app.router.get('/delete', 'httpclient.del');
  app.router.get('/form', 'httpclient.form');
  app.router.get('/multipart', 'httpclient.multipart');
  app.router.get('/files', 'httpclient.files');
  app.router.get('/stream', 'httpclient.stream');
  app.router.post('/stream', 'httpclient.postStream');
  app.router.get('/error', 'httpclient.error');
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# 参考链接

https://eggjs.org/zh-cn/core/httpclient.html

上次更新: 2022/04/15, 05:41:27
×