互聯(lián)網(wǎng)時(shí)代,無(wú)數(shù)服務(wù)是基于 HTTP 協(xié)議進(jìn)行通信的,Web 應(yīng)用調(diào)用后端 HTTP 服務(wù)是一種非常常見的應(yīng)用場(chǎng)景。
為此框架基于 urllib 內(nèi)置實(shí)現(xiàn)了一個(gè) HttpClient,應(yīng)用可以非常便捷地完成任何 HTTP 請(qǐng)求。
通過(guò) app 使用 HttpClient
框架在應(yīng)用初始化的時(shí)候,會(huì)自動(dòng)將 HttpClient 初始化到 app.httpclient。 同時(shí)增加了一個(gè) app.curl(url, options) 方法,它等價(jià)于 app.httpclient.request(url, options)。
這樣就可以非常方便地使用 app.curl 方法完成一次 HTTP 請(qǐng)求。
// app.js module.exports = app => { app.beforeStart(async () => { // 示例:?jiǎn)?dòng)的時(shí)候去讀取 https://registry.npm.taobao.org/egg/latest 的版本信息 const result = await app.curl('https://registry.npm.taobao.org/egg/latest', { dataType: 'json', }); app.logger.info('Egg latest version: %s', result.data.version); }); };
|
通過(guò) ctx 使用 HttpClient
框架在 Context 中同樣提供了 ctx.curl(url, options) 和 ctx.httpclient,保持跟 app 下的使用體驗(yàn)一致。 這樣就可以在有 Context 的地方(如在 controller 中)非常方便地使用 ctx.curl() 方法完成一次 HTTP 請(qǐng)求。
// app/controller/npm.js class NpmController extends Controller { async index() { const ctx = this.ctx;
// 示例:請(qǐng)求一個(gè) npm 模塊信息 const result = await ctx.curl('https://registry.npm.taobao.org/egg/latest', { // 自動(dòng)解析 JSON response dataType: 'json', // 3 秒超時(shí) timeout: 3000, });
ctx.body = { status: result.status, headers: result.headers, package: result.data, }; } }
|
基本 HTTP 請(qǐng)求
HTTP 已經(jīng)被廣泛大量使用,盡管 HTTP 有多種請(qǐng)求方式,但是萬(wàn)變不離其宗,我們先以基本的4個(gè)請(qǐng)求方法為例子, 逐步講解一下更多的復(fù)雜應(yīng)用場(chǎng)景。
以下例子都會(huì)在 controller 代碼中對(duì) https://httpbin.org 發(fā)起請(qǐng)求來(lái)完成。
GET
讀取數(shù)據(jù)幾乎都是使用 GET 請(qǐng)求,它是 HTTP 世界最常見的一種,也是最廣泛的一種,它的請(qǐng)求參數(shù)也是最容易構(gòu)造的。
// app/controller/npm.js class NpmController extends Controller { async get() { const ctx = this.ctx; const result = await ctx.curl('https://httpbin.org/get?foo=bar'); ctx.status = result.status; ctx.set(result.headers); ctx.body = result.data; } }
|
- GET 請(qǐng)求可以不用設(shè)置 options.method 參數(shù),HttpClient 的默認(rèn) method 會(huì)設(shè)置為 GET。
- 返回值 result 會(huì)包含 3 個(gè)屬性:status, headers 和 datastatus: 響應(yīng)狀態(tài)碼,如 200, 302, 404, 500 等等headers: 響應(yīng)頭,類似 { 'content-type': 'text/html', ... }data: 響應(yīng) body,默認(rèn) HttpClient 不會(huì)做任何處理,會(huì)直接返回 Buffer 類型數(shù)據(jù)。 一旦設(shè)置了 options.dataType,HttpClient 將會(huì)根據(jù)此參數(shù)對(duì) data 進(jìn)行相應(yīng)的處理。
完整的請(qǐng)求參數(shù) options 和返回值 result 的說(shuō)明請(qǐng)看下文的 options 參數(shù)詳解 章節(jié)。
POST
創(chuàng)建數(shù)據(jù)的場(chǎng)景一般來(lái)說(shuō)都會(huì)使用 POST 請(qǐng)求,它相對(duì)于 GET 來(lái)說(shuō)多了請(qǐng)求 body 這個(gè)參數(shù)。
以發(fā)送 JSON body 的場(chǎng)景舉例:
// app/controller/npm.js class NpmController extends Controller { async post() { const ctx = this.ctx; const result = await ctx.curl('https://httpbin.org/post', { // 必須指定 method method: 'POST', // 通過(guò) contentType 告訴 HttpClient 以 JSON 格式發(fā)送 contentType: 'json', data: { hello: 'world', now: Date.now(), }, // 明確告訴 HttpClient 以 JSON 格式處理返回的響應(yīng) body dataType: 'json', }); ctx.body = result.data; } }
|
下文還會(huì)詳細(xì)講解以 POST 實(shí)現(xiàn) Form 表單提交和文件上傳的功能。
PUT
PUT 與 POST 類似,它更加適合更新數(shù)據(jù)和替換數(shù)據(jù)的語(yǔ)義。 除了 method 參數(shù)需要設(shè)置為 PUT,其他參數(shù)幾乎跟 POST 一模一樣。
// app/controller/npm.js class NpmController extends Controller { async put() { const ctx = this.ctx; const result = await ctx.curl('https://httpbin.org/put', { // 必須指定 method method: 'PUT', // 通過(guò) contentType 告訴 HttpClient 以 JSON 格式發(fā)送 contentType: 'json', data: { update: 'foo bar', }, // 明確告訴 HttpClient 以 JSON 格式處理響應(yīng) body dataType: 'json', }); ctx.body = result.data; } }
|
DELETE
刪除數(shù)據(jù)會(huì)選擇 DELETE 請(qǐng)求,它通??梢圆恍枰诱?qǐng)求 body,但是 HttpClient 不會(huì)限制。
// app/controller/npm.js class NpmController extends Controller { async del() { const ctx = this.ctx; const result = await ctx.curl('https://httpbin.org/delete', { // 必須指定 method method: 'DELETE', // 明確告訴 HttpClient 以 JSON 格式處理響應(yīng) body dataType: 'json', }); ctx.body = result.data; } }
|
高級(jí) HTTP 請(qǐng)求
在真實(shí)的應(yīng)用場(chǎng)景下,還是會(huì)包含一些較為復(fù)雜的 HTTP 請(qǐng)求。
Form 表單提交
面向?yàn)g覽器設(shè)計(jì)的 Form 表單(不包含文件)提交接口,通常都要求以 content-type: application/x-www-form-urlencoded 的格式提交請(qǐng)求數(shù)據(jù)。
// app/controller/npm.js class NpmController extends Controller { async submit() { const ctx = this.ctx; const result = await ctx.curl('https://httpbin.org/post', { // 必須指定 method,支持 POST,PUT 和 DELETE method: 'POST', // 不需要設(shè)置 contentType,HttpClient 會(huì)默認(rèn)以 application/x-www-form-urlencoded 格式發(fā)送請(qǐng)求 data: { now: Date.now(), foo: 'bar', }, // 明確告訴 HttpClient 以 JSON 格式處理響應(yīng) body dataType: 'json', }); ctx.body = result.data.form; // 響應(yīng)最終會(huì)是類似以下的結(jié)果: // { // "foo": "bar", // "now": "1483864184348" // } } }
|
以 Multipart 方式上傳文件
當(dāng)一個(gè) Form 表單提交包含文件的時(shí)候,請(qǐng)求數(shù)據(jù)格式就必須以 multipart/form-data 進(jìn)行提交了。
urllib 內(nèi)置了 formstream 模塊來(lái)幫助我們生成可以被消費(fèi)的 form 對(duì)象。
// app/controller/http.js class HttpController extends Controller { async upload() { const { ctx } = this;
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; // 響應(yīng)最終會(huì)是類似以下的結(jié)果: // { // "file": "'use strict';\n\nconst For...." // } } }
|
以 Stream 方式上傳文件
其實(shí),在 Node.js 的世界里面,Stream 才是主流。 如果服務(wù)端支持流式上傳,最友好的方式還是直接發(fā)送 Stream。 Stream 實(shí)際會(huì)以 Transfer-Encoding: chunked 傳輸編碼格式發(fā)送,這個(gè)轉(zhuǎn)換是 HTTP 模塊自動(dòng)實(shí)現(xiàn)的。
// app/controller/npm.js const fs = require('fs'); const FormStream = require('formstream'); class NpmController extends Controller { async uploadByStream() { const ctx = this.ctx; // 上傳當(dāng)前文件本身用于測(cè)試 const fileStream = fs.createReadStream(__filename); // httpbin.org 不支持 stream 模式,使用本地 stream 接口代替 const url = `${ctx.protocol}://${ctx.host}/stream`; const result = await ctx.curl(url, { // 必須指定 method,支持 POST,PUT method: 'POST', // 以 stream 模式提交 stream: fileStream, }); ctx.status = result.status; ctx.set(result.headers); ctx.body = result.data; // 響應(yīng)最終會(huì)是類似以下的結(jié)果: // {"streamSize":574} } }
|
options 參數(shù)詳解
由于 HTTP 請(qǐng)求的復(fù)雜性,導(dǎo)致 httpclient.request(url, options) 的 options 參數(shù)會(huì)非常多。 接下來(lái)將會(huì)以參數(shù)說(shuō)明和代碼配合一起講解每個(gè)可選參數(shù)的實(shí)際用途。
HttpClient 默認(rèn)全局配置
// config/config.default.js exports.httpclient = { // 是否開啟本地 DNS 緩存,默認(rèn)關(guān)閉,開啟后有兩個(gè)特性 // 1. 所有的 DNS 查詢都會(huì)默認(rèn)優(yōu)先使用緩存的,即使 DNS 查詢錯(cuò)誤也不影響應(yīng)用 // 2. 對(duì)同一個(gè)域名,在 dnsCacheLookupInterval 的間隔內(nèi)(默認(rèn) 10s)只會(huì)查詢一次 enableDNSCache: false, // 對(duì)同一個(gè)域名進(jìn)行 DNS 查詢的最小間隔時(shí)間 dnsCacheLookupInterval: 10000, // DNS 同時(shí)緩存的最大域名數(shù)量,默認(rèn) 1000 dnsCacheMaxLength: 1000,
request: { // 默認(rèn) request 超時(shí)時(shí)間 timeout: 3000, },
httpAgent: { // 默認(rèn)開啟 http KeepAlive 功能 keepAlive: true, // 空閑的 KeepAlive socket 最長(zhǎng)可以存活 4 秒 freeSocketTimeout: 4000, // 當(dāng) socket 超過(guò) 30 秒都沒(méi)有任何活動(dòng),就會(huì)被當(dāng)作超時(shí)處理掉 timeout: 30000, // 允許創(chuàng)建的最大 socket 數(shù) maxSockets: Number.MAX_SAFE_INTEGER, // 最大空閑 socket 數(shù) maxFreeSockets: 256, },
httpsAgent: { // 默認(rèn)開啟 https KeepAlive 功能 keepAlive: true, // 空閑的 KeepAlive socket 最長(zhǎng)可以存活 4 秒 freeSocketTimeout: 4000, // 當(dāng) socket 超過(guò) 30 秒都沒(méi)有任何活動(dòng),就會(huì)被當(dāng)作超時(shí)處理掉 timeout: 30000, // 允許創(chuàng)建的最大 socket 數(shù) maxSockets: Number.MAX_SAFE_INTEGER, // 最大空閑 socket 數(shù) maxFreeSockets: 256, }, };
|
應(yīng)用可以通過(guò) config/config.default.js 覆蓋此配置。
data: Object
需要發(fā)送的請(qǐng)求數(shù)據(jù),根據(jù) method 自動(dòng)選擇正確的數(shù)據(jù)處理方式。
- GET,HEAD:通過(guò) querystring.stringify(data) 處理后拼接到 url 的 query 參數(shù)上。
- POST,PUT 和 DELETE 等:需要根據(jù) contentType 做進(jìn)一步判斷處理。contentType = json:通過(guò) JSON.stringify(data) 處理,并設(shè)置為 body 發(fā)送。其他:通過(guò) querystring.stringify(data) 處理,并設(shè)置為 body 發(fā)送。
// GET + data ctx.curl(url, { data: { foo: 'bar' }, });
// POST + data ctx.curl(url, { method: 'POST', data: { foo: 'bar' }, });
// POST + JSON + data ctx.curl(url, { method: 'POST', contentType: 'json', data: { foo: 'bar' }, });
|
dataAsQueryString: Boolean
如果設(shè)置了 dataAsQueryString=true,那么即使在 POST 情況下, 也會(huì)強(qiáng)制將 options.data 以 querystring.stringify 處理之后拼接到 url 的 query 參數(shù)上。
可以很好地解決以 stream 發(fā)送數(shù)據(jù),且額外的請(qǐng)求參數(shù)以 url query 形式傳遞的應(yīng)用場(chǎng)景:
ctx.curl(url, { method: 'POST', dataAsQueryString: true, data: { // 一般來(lái)說(shuō)都是 access token 之類的權(quán)限驗(yàn)證參數(shù) accessToken: 'some access token value', }, stream: myFileStream, });
|
content: String|Buffer
發(fā)送請(qǐng)求正文,如果設(shè)置了此參數(shù),那么會(huì)直接忽略 data 參數(shù)。
ctx.curl(url, { method: 'POST', // 直接發(fā)送原始 xml 數(shù)據(jù),不需要 HttpClient 做特殊處理 content: '<xml><hello>world</hello></xml>', headers: { 'content-type': 'text/html', }, });
|
files: Mixed
文件上傳,支持格式: String | ReadStream | Buffer | Array | Object。
ctx.curl(url, { method: 'POST', files: '/path/to/read', data: { foo: 'other fields', }, });
|
多文件上傳:
ctx.curl(url, { method: 'POST', files: { file1: '/path/to/read', file2: fs.createReadStream(__filename), file3: Buffer.from('mock file content'), }, data: { foo: 'other fields', }, });
|
stream: ReadStream
設(shè)置發(fā)送請(qǐng)求正文的可讀數(shù)據(jù)流,默認(rèn)是 null。 一旦設(shè)置了此參數(shù),HttpClient 將會(huì)忽略 data 和 content。
ctx.curl(url, { method: 'POST', stream: fs.createReadStream('/path/to/read'), });
|
writeStream: WriteStream
設(shè)置接受響應(yīng)數(shù)據(jù)的可寫數(shù)據(jù)流,默認(rèn)是 null。 一旦設(shè)置此參數(shù),那么返回值 result.data 將會(huì)被設(shè)置為 null, 因?yàn)閿?shù)據(jù)已經(jīng)全部寫入到 writeStream 中了。
ctx.curl(url, { writeStream: fs.createWriteStream('/path/to/store'), });
|
consumeWriteStream: Boolean
是否等待 writeStream 完全寫完才算響應(yīng)全部接收完畢,默認(rèn)是 true。 此參數(shù)不建議修改默認(rèn)值,除非我們明確知道它的副作用是可接受的, 否則很可能會(huì)導(dǎo)致 writeStream 數(shù)據(jù)不完整。
method: String
設(shè)置請(qǐng)求方法,默認(rèn)是 GET。 支持 GET、POST、PUT、DELETE、PATCH 等所有 HTTP 方法。
contentType: String
設(shè)置請(qǐng)求數(shù)據(jù)格式,默認(rèn)是 undefined,HttpClient 會(huì)自動(dòng)根據(jù) data 和 content 參數(shù)自動(dòng)設(shè)置。 data 是 object 的時(shí)候默認(rèn)設(shè)置的是 form。支持 json 格式。
如需要以 JSON 格式發(fā)送 data:
ctx.curl(url, { method: 'POST', data: { foo: 'bar', now: Date.now(), }, contentType: 'json', });
|
dataType: String
設(shè)置響應(yīng)數(shù)據(jù)格式,默認(rèn)不對(duì)響應(yīng)數(shù)據(jù)做任何處理,直接返回原始的 buffer 格式數(shù)據(jù)。 支持 text 和 json 兩種格式。
注意:設(shè)置成 json 時(shí),如果響應(yīng)數(shù)據(jù)解析失敗會(huì)拋 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);
|
fixJSONCtlChars: Boolean
是否自動(dòng)過(guò)濾響應(yīng)數(shù)據(jù)中的特殊控制字符 (U+0000 ~ U+001F),默認(rèn)是 false。 通常一些 CGI 系統(tǒng)返回的 JSON 數(shù)據(jù)會(huì)包含這些特殊控制字符,通過(guò)此參數(shù)可以自動(dòng)過(guò)濾掉它們。
ctx.curl(url, { fixJSONCtlChars: true, dataType: 'json', });
|
headers: Object
自定義請(qǐng)求頭。
ctx.curl(url, { headers: { 'x-foo': 'bar', }, });
|
timeout: Number|Array
請(qǐng)求超時(shí)時(shí)間,默認(rèn)是 [ 5000, 5000 ],即創(chuàng)建連接超時(shí)是 5 秒,接收響應(yīng)超時(shí)是 5 秒。
ctx.curl(url, { // 創(chuàng)建連接超時(shí) 3 秒,接收響應(yīng)超時(shí) 3 秒 timeout: 3000, });
ctx.curl(url, { // 創(chuàng)建連接超時(shí) 1 秒,接收響應(yīng)超時(shí) 30 秒,用于響應(yīng)比較大的場(chǎng)景 timeout: [ 1000, 30000 ], });
|
agent: HttpAgent
允許通過(guò)此參數(shù)覆蓋默認(rèn)的 HttpAgent,如果你不想開啟 KeepAlive,可以設(shè)置此參數(shù)為 false。
ctx.curl(url, { agent: false, });
|
httpsAgent: HttpsAgent
允許通過(guò)此參數(shù)覆蓋默認(rèn)的 HttpsAgent,如果你不想開啟 KeepAlive,可以設(shè)置此參數(shù)為 false。
ctx.curl(url, { httpsAgent: false, });
|
auth: String
簡(jiǎn)單登錄授權(quán)(Basic Authentication)參數(shù),將以明文方式將登錄信息以 Authorization 請(qǐng)求頭發(fā)送出去。
ctx.curl(url, { // 參數(shù)必須按照 `user:password` 格式設(shè)置 auth: 'foo:bar', });
|
digestAuth: String
摘要登錄授權(quán)(Digest Authentication)參數(shù),設(shè)置此參數(shù)會(huì)自動(dòng)對(duì) 401 響應(yīng)嘗試生成 Authorization 請(qǐng)求頭, 嘗試以授權(quán)方式請(qǐng)求一次。
ctx.curl(url, { // 參數(shù)必須按照 `user:password` 格式設(shè)置 digestAuth: 'foo:bar', });
|
followRedirect: Boolean
是否自動(dòng)跟進(jìn) 3xx 的跳轉(zhuǎn)響應(yīng),默認(rèn)是 false。
ctx.curl(url, { followRedirect: true, });
|
maxRedirects: Number
設(shè)置最大自動(dòng)跳轉(zhuǎn)次數(shù),避免循環(huán)跳轉(zhuǎn)無(wú)法終止,默認(rèn)是 10 次。 此參數(shù)不宜設(shè)置過(guò)大,它只在 followRedirect=true 情況下才會(huì)生效。
ctx.curl(url, { followRedirect: true, // 最大只允許自動(dòng)跳轉(zhuǎn) 5 次。 maxRedirects: 5, });
|
formatRedirectUrl: Function(from, to)
允許我們通過(guò) formatRedirectUrl 自定義實(shí)現(xiàn) 302、301 等跳轉(zhuǎn) url 拼接, 默認(rèn)是 url.resolve(from, to)。
ctx.curl(url, { formatRedirectUrl: (from, to) => { // 例如可在這里修正跳轉(zhuǎn)不正確的 url if (to === '//foo/') { to = '/foo'; } return url.resolve(from, to); }, });
|
beforeRequest: Function(options)
HttpClient 在請(qǐng)求正式發(fā)送之前,會(huì)嘗試調(diào)用 beforeRequest 鉤子,允許我們?cè)谶@里對(duì)請(qǐng)求參數(shù)做最后一次修改。
ctx.curl(url, { beforeRequest: options => { // 例如我們可以設(shè)置全局請(qǐng)求 id,方便日志跟蹤 options.headers['x-request-id'] = uuid.v1(); }, });
|
streaming: Boolean
是否直接返回響應(yīng)流,默認(rèn)為 false。 開啟 streaming 之后,HttpClient 會(huì)在拿到響應(yīng)對(duì)象 res 之后馬上返回, 此時(shí) result.headers 和 result.status 已經(jīng)可以讀取到,只是沒(méi)有讀取 data 數(shù)據(jù)而已。
const result = await ctx.curl(url, { streaming: true, });
console.log(result.status, result.data); // result.res 是一個(gè) ReadStream 對(duì)象 ctx.body = result.res;
|
注意:如果 res 不是直接傳遞給 body,那么我們必須消費(fèi)這個(gè) stream,并且要做好 error 事件處理。
gzip: Boolean
是否支持 gzip 響應(yīng)格式,默認(rèn)為 false。 開啟 gzip 之后,HttpClient 將自動(dòng)設(shè)置 Accept-Encoding: gzip 請(qǐng)求頭, 并且會(huì)自動(dòng)解壓帶 Content-Encoding: gzip 響應(yīng)頭的數(shù)據(jù)。
ctx.curl(url, { gzip: true, });
|
timing: Boolean
是否開啟請(qǐng)求各階段的時(shí)間測(cè)量,默認(rèn)為 false。 開啟 timing 之后,可以通過(guò) result.res.timing 拿到這次 HTTP 請(qǐng)求各階段的時(shí)間測(cè)量值(單位是毫秒), 通過(guò)這些測(cè)量值,我們可以非常方便地定位到這次請(qǐng)求最慢的環(huán)境發(fā)生在那個(gè)階段,效果如同 Chrome network timing 的作用。
timing 各階段測(cè)量值解析:
- queuing:分配 socket 耗時(shí)
- dnslookup:DNS 查詢耗時(shí)
- connected:socket 三次握手連接成功耗時(shí)
- requestSent:請(qǐng)求數(shù)據(jù)完整發(fā)送完畢耗時(shí)
- waiting:收到第一個(gè)字節(jié)的響應(yīng)數(shù)據(jù)耗時(shí)
- contentDownload:全部響應(yīng)數(shù)據(jù)接收完畢耗時(shí)
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 // }
|
ca,rejectUnauthorized,pfx,key,cert,passphrase,ciphers,secureProtocol
這幾個(gè)都是透?jìng)鹘o HTTPS 模塊的參數(shù),具體請(qǐng)查看 https.request(options, callback)。
調(diào)試輔助
如果你需要對(duì) HttpClient 的請(qǐng)求進(jìn)行抓包調(diào)試,可以添加以下配置到 config.local.js:
// 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; }
|
然后啟動(dòng)你的抓包工具,如 charles 或 fiddler。
最后通過(guò)以下指令啟動(dòng)應(yīng)用:
$ http_proxy=http://127.0.0.1:8888 npm run dev
|
然后就可以正常操作了,所有經(jīng)過(guò) HttpClient 的請(qǐng)求,都可以你的抓包工具中查看到。
常見錯(cuò)誤
創(chuàng)建連接超時(shí)
- 異常名稱:ConnectionTimeoutError
- 出現(xiàn)場(chǎng)景:通常是 DNS 查詢比較慢,或者客戶端與服務(wù)端之間的網(wǎng)絡(luò)速度比較慢導(dǎo)致的。
- 排查建議:請(qǐng)適當(dāng)增大 timeout 參數(shù)。
服務(wù)響應(yīng)超時(shí)
- 異常名稱:ResponseTimeoutError
- 出現(xiàn)場(chǎng)景:通常是客戶端與服務(wù)端之間網(wǎng)絡(luò)速度比較慢,并且響應(yīng)數(shù)據(jù)比較大的情況下會(huì)發(fā)生。
- 排查建議:請(qǐng)適當(dāng)增大 timeout 參數(shù)。
服務(wù)主動(dòng)斷開連接
- 異常名稱:ResponseError, code: ECONNRESET
- 出現(xiàn)場(chǎng)景:通常是服務(wù)端主動(dòng)斷開 socket 連接,導(dǎo)致 HTTP 請(qǐng)求鏈路異常。
- 排查建議:請(qǐng)檢查當(dāng)時(shí)服務(wù)端是否發(fā)生網(wǎng)絡(luò)異常。
服務(wù)不可達(dá)
- 異常名稱:RequestError, code: ECONNREFUSED, status: -1
- 出現(xiàn)場(chǎng)景:通常是因?yàn)檎?qǐng)求的 url 所屬 IP 或者端口無(wú)法連接成功。
- 排查建議:請(qǐng)確保 IP 或者端口設(shè)置正確。
域名不存在
- 異常名稱:RequestError, code: ENOTFOUND, status: -1
- 出現(xiàn)場(chǎng)景:通常是因?yàn)檎?qǐng)求的 url 所在的域名無(wú)法通過(guò) DNS 解析成功。
- 排查建議:請(qǐng)確保域名存在,也需要排查一下 DNS 服務(wù)是否配置正確。
JSON 響應(yīng)數(shù)據(jù)格式錯(cuò)誤
- 異常名稱:JSONResponseFormatError
- 出現(xiàn)場(chǎng)景:設(shè)置了 dataType=json 并且響應(yīng)數(shù)據(jù)不符合 JSON 格式,就會(huì)拋出此異常。
- 排查建議:確保服務(wù)端無(wú)論在什么情況下都要正確返回 JSON 格式的數(shù)據(jù)。
全局 request 和 response 事件
在企業(yè)應(yīng)用場(chǎng)景,常常會(huì)有統(tǒng)一 tracer 日志的需求。 為了方便在 app 層面統(tǒng)一監(jiān)聽 HttpClient 的請(qǐng)求和響應(yīng),我們約定了全局 request 和 response 來(lái)暴露這兩個(gè)事件。
init options | V emit `request` event | V send request and receive response | V emit `response` event | V end
|
request 事件:發(fā)生在網(wǎng)絡(luò)操作發(fā)生之前
請(qǐng)求發(fā)送之前,會(huì)觸發(fā)一個(gè) request 事件,允許對(duì)請(qǐng)求做攔截。
app.httpclient.on('request', req => { req.url //請(qǐng)求 url req.ctx //是發(fā)起這次請(qǐng)求的當(dāng)前上下文
// 可以在這里設(shè)置一些 trace headers,方便全鏈路跟蹤 });
|
response 事件:發(fā)生在網(wǎng)絡(luò)操作結(jié)束之后
請(qǐng)求結(jié)束之后會(huì)觸發(fā)一個(gè) response 事件,這樣外部就可以訂閱這個(gè)事件打印日志。
app.httpclient.on('response', result => { result.res.status result.ctx //是發(fā)起這次請(qǐng)求的當(dāng)前上下文 result.req //對(duì)應(yīng)的 req 對(duì)象,即 request 事件里面那個(gè) req });
|
示例代碼
完整示例代碼可以在 eggjs/examples/httpclient 找到。
更多建議: