node發(fā)起axios請求報錯:socket-hang-up

看這一段人畜無害的代碼,axios的引入和配置,再加上超長的超時限制

import axios from 'axios'
let baseUrl = 'https://XXX.com的接口地址'
const request = axios.create({
   baseURL: baseUrl, //基準地址
   timeout: 500000
})
// 請求攔截
request.interceptors.request.use((config) => {
   return config
})
// 響應(yīng)攔截
request.interceptors.response.use(
   (res) => {
      switch (res.status) {
         case '500':
            // Toast.fail("服務(wù)器錯誤")
            return
         case '404':
         // Toast.fail("頁面丟失了~~")
      }
      return res
   },
   (error) => {
      // 錯誤處理
      if (error && error.response) {
         console.log(error.response)
      }
      return Promise.reject(error)
   }
)
export default request

那么問題來了,第一個請求之后,node處理了很多邏輯操作,以至于要等好久才會發(fā)起第二次請求,第二次請求的攔截已經(jīng)可以打印config,但是后面完全沒有響應(yīng)了,并且報錯:socket hang up

result: 'error',
data: AxiosError: socket hang up
    at AxiosError.from (file:///xxx本地路徑/node_modules/axios/lib/core/AxiosError.js:89:14)
    at RedirectableRequest.handleRequestError (file:///xxx本地路徑/node_modules/axios/lib/adapters/http.js:610:25)
    at RedirectableRequest.emit (node:events:531:35)
    at RedirectableRequest.emit (node:domain:488:12)
    at eventHandlers.<computed> (xxx本地路徑/node_modules/follow-redirects/index.js:38:24)
    at ClientRequest.emit (node:events:519:28)
    at ClientRequest.emit (node:domain:488:12)
    at TLSSocket.socketOnEnd (node:_http_client:524:9)
    at TLSSocket.emit (node:events:531:35)
    at TLSSocket.emit (node:domain:488:12)
    at Axios.request (file:///xxx本地路徑/node_modules/axios/lib/core/Axios.js:45:41)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async GetServiceInfo (file:///xxx本地路徑/src/service/index.js:26:9)
    at async HandleEventList (file:///xxx本地路徑/src/creatproject/HandleEventList.js:92:47)
    at async HandleTreeData (file:///xxx本地路徑/src/creatproject/HandlePageData.js:81:25)
    at async handlePageData (file:///xxx本地路徑/src/creatproject/HandlePageData.js:37:5)
    at async CreatProject.creatRN (file:///xxx本地路徑/src/creatproject/CreatProject.js:128:4) {
    code: 'ECONNRESET',
        config: {
    transitional: [Object],
        adapter: [Array],
        transformRequest: [Array],
        transformResponse: [Array],
        timeout: 50000,
        xsrfCookieName: 'XSRF-TOKEN',
        xsrfHeaderName: 'X-XSRF-TOKEN',
        maxContentLength: -1,
        maxBodyLength: -1,
        env: [Object],
        validateStatus: [Function: validateStatus],
    headers: [Object [AxiosHeaders]],
        baseURL: 'https://www不能告訴你的接口地址',
        url: '/zzz不能告訴你的路徑',
        method: 'post',
        data: '{...參數(shù)}'
    },
    request: Writable {
        _events: [Object],
            _writableState: [WritableState],
            _maxListeners: undefined,
            _options: [Object],
            _ended: true,
            _ending: true,
            _redirectCount: 0,
            _redirects: [],
            _requestBodyLength: 111,
            _requestBodyBuffers: [Array],
            _eventsCount: 3,
            _onNativeResponse: [Function (anonymous)],
            _currentRequest: [ClientRequest],
            _currentUrl: 'https://www不能告訴你的接口地址/zzz不能告訴你的路徑',
            _timeout: null,
            [Symbol(shapeMode)]: true,
            [Symbol(kCapture)]: false
        },
        cause: Error: socket hang up
        at TLSSocket.socketOnEnd (node:_http_client:524:23)
        at TLSSocket.emit (node:events:531:35)
        at TLSSocket.emit (node:domain:488:12)
        at endReadableNT (node:internal/streams/readable:1696:12)
        at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
            code: 'ECONNRESET'
        }
    }

最喜歡疑難雜癥,因為比較有意思,但也最怕這種坑,因為老板總是催業(yè)務(wù),不敢耽擱太久。

一、我們先盤一下問題

在瀏覽器中,Axios使用 XMLHttpRequest 對象作為其底層的 HTTP 客戶端。XMLHttpRequest 是瀏覽器提供的原生對象,用于在客戶端發(fā)起 HTTP 請求。在 Node.js 環(huán)境中,Axios使用 http 模塊或 https 模塊作為其底層的 HTTP 客戶端,具體取決于請求的協(xié)議是 HTTP 還是 HTTPS。這些模塊提供了在 Node.js 環(huán)境中發(fā)起 HTTP 請求所需的功能。因此,無論是在瀏覽器端還是在 Node.js 環(huán)境中,Axios都會使用適當(dāng)?shù)?Http client實現(xiàn)網(wǎng)絡(luò)請求。所以我們暫且把他倆當(dāng)作同一個情況。

什么是 Socket hang up

說不定以后也可以做為面試題目來提問,什么是 Socket hang up?

hang up 翻譯為英文有掛斷的意思, socket hang up 也可以理解為 socket(鏈接)被掛斷。無論使用哪種語言,也許多多少少應(yīng)該都會遇見過,只是不知道你有沒有去思考這是為什么?例如在 Node.js 中系統(tǒng)提供的 http server 默認超時為 2 分鐘(server.timeout 可以查看),如果一個請求超出這個時間,http server 會關(guān)閉這個請求鏈接,當(dāng)客戶端想要返回一個請求的時候發(fā)現(xiàn)這個 socket 已經(jīng)被 “掛斷”,就會報 socket hang up 錯誤。

問題復(fù)現(xiàn)

比較偶現(xiàn),但是本地實際應(yīng)用中,兩次請求間隔時間較長,再通過調(diào)整timeout大小還是比較穩(wěn)定復(fù)現(xiàn)的。

網(wǎng)上有一種復(fù)現(xiàn)方式有興趣可以試試

1、服務(wù)端:開啟一個 http 服務(wù),定義 /timeout 接口設(shè)置 3 分鐘之后延遲響應(yīng),也就是讓它3米分鐘后再response;

2、客戶端:創(chuàng)建一個http請求并監(jiān)聽請求和error;

3、操作:啟動服務(wù)端之后再啟動客戶端大約 2 分鐘之后或者直接 kill 掉服務(wù)端會報Soket hang up的錯

錯誤原因

為什么在 http client 這一端會報 socket hang up 這個錯誤,看下 Node.js http client 端源碼會發(fā)現(xiàn)由于沒有得到響應(yīng),那么就認為這個 socket 已經(jīng)結(jié)束,因此會觸發(fā)一個 connResetException('socket hang up') 錯誤。

// https://github.com/nodejs/node/blob/v12.x/lib/_http_client.js#L440
function socketOnEnd() {
  const socket = this;
  const req = this._httpMessage;
  const parser = this.parser;
  if (!req.res && !req.socket._hadError) {
    // If we don't have a response then we know that the socket
    // ended prematurely and we need to emit an error on the request.
    req.socket._hadError = true;
    req.emit('error', connResetException('socket hang up'));
  }
  if (parser) {
    parser.finish();
    freeParser(parser, req, socket);
  }
  socket.destroy();
}

二、解決問題的過程

1、設(shè)置 http server socket 超時時間(不建議)

看以下 Node.js http server 源碼,默認情況下服務(wù)器的超時值為 2 分鐘,如果超時,socket 會自動銷毀,可以通過調(diào)用 server.setTimeout(msecs) 方法將超時時間調(diào)節(jié)大一些,如果傳入 0 將關(guān)閉超時機制。

// https://github.com/nodejs/node/blob/v12.x/lib/_http_server.js#L348
function Server(options, requestListener) {
  // ...
  this.timeout = kDefaultHttpServerTimeout; // 默認為 2 * 60 * 1000
  this.keepAliveTimeout = 5000;
  this.maxHeadersCount = null;
  this.headersTimeout = 40 * 1000; // 40 seconds
}
Object.setPrototypeOf(Server.prototype, net.Server.prototype);
Object.setPrototypeOf(Server, net.Server);

Server.prototype.setTimeout = function setTimeout(msecs, callback) {
  this.timeout = msecs;
  if (callback)
    this.on('timeout', callback);
  return this;
};

但是這種不受限的連接,明顯不安全呀,而且也會很占用資源,這種做法我是不敢用的,哈哈哈。如果不設(shè)置 setTimeout 也可以針對這種錯誤在 http client 端進行捕獲放入隊列發(fā)起重試,當(dāng)這種錯誤概率很大的時候要去排查相應(yīng)的服務(wù)是否存在處理很慢等異常問題。

2、關(guān)閉keep-alive

這點我們針對axios的,在請求的攔截中我們看到它默認開啟了keep-alive,持久連接。雙方都可以發(fā)起持久連接的請求,但是雙方也都可以拒絕持久連接。通俗說就是我可以請求你在我閑著的時候保持通話,但是你也可以在我閑著沒聲兒的時候直接掛我電話的意思,反之亦然。有大佬特別耐心地抓了網(wǎng)絡(luò)包,并分析了tcp包的數(shù)據(jù)傳輸過程,有興趣可以了解一下。,

當(dāng)然服務(wù)器是支持的,而且postman發(fā)請求一直都是沒問題的。只是為了避免在長連接中出現(xiàn)socket變化,導(dǎo)致ECONNRESET問題,我們可以直接關(guān)閉,讓每次請求都重新進行三次握手:

import axios from 'axios'
import https from 'https'
request.interceptors.request.use(
    (config) => {
        //在請求攔截中設(shè)置
        config.httpsAgent = new https.Agent({ keepAlive: false });
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
)

不過這樣也有問題,畢竟長連接的作用本來就是減少請求時間和資源消耗,對于一些c端場景可能并不是最優(yōu)解。

3、啟用axios-retry(最優(yōu)解)

axios-retry是一個用于在Node.js和瀏覽器中使用的Axios插件,它提供了在HTTP請求失敗時自動重試的功能。通過使用axios-retry,您可以配置Axios實例以在遇到連接問題或其他臨時錯誤時自動重試請求,以增加請求的可靠性。

同時我們在請求攔截中增加對重試請求的監(jiān)聽或者響應(yīng)里的錯誤監(jiān)聽,通過日志來判斷網(wǎng)絡(luò)請求的穩(wěn)定性和觸達率。

import axios from 'axios'
import axiosRetry from 'axios-retry'
let baseUrl = 'https://XXX.com的接口地址'
const request = axios.create({
   baseURL: baseUrl, //基準地址
   timeout: 500000
})
// 設(shè)置axios-retry的配置
axiosRetry(request, {
    retries: 3, // 設(shè)置重試次數(shù)
    retryDelay: axiosRetry.exponentialDelay, // 設(shè)置重試延遲策略
});

// 請求攔截
request.interceptors.request.use((config) => {
    if (config.__isRetryRequest) {
        // 這里是針對重試請求的監(jiān)聽
        logger.info('重試請求', config.url)
    }
   return config
})
// 響應(yīng)攔截
request.interceptors.response.use(
   (res) => {
      // 。 。 。
      return res
   },
   (error) => {
      if (error.code === 'ECONNABORTED') {
         // 監(jiān)聽Socket hang up的錯誤日志
         logger.error('sockt錯誤', error)
      } else{
         // 。 。 。
      }
      return Promise.reject(error)
   }
)

三、思考一下

除了上面提到的可行或不可行的方法,處理過程中還嘗試去看node版本和axios版本對它的影響,但是都沒有絕對的處理掉,并且在github上關(guān)于axios的這個問題也有討論,并且現(xiàn)在的bug狀態(tài)還是open,應(yīng)該是還沒解決。

axios源碼中看到有這么一段話,翻譯是說:

有時響應(yīng)會很慢,如果不響應(yīng),連接事件就會被事件循環(huán)系統(tǒng)阻塞;

計時器回調(diào)將被觸發(fā),并在連接之前調(diào)用abort(),然后獲得“socket hang up”和code碼ECONNRESET;

此時,如果我們有大量的請求,nodejs會在后臺掛起一些socket。這個數(shù)字會越來越大。
然后這些被掛起的套接字就會一點點地吞噬CPU;

ClientRequest.setTimeout將在指定的毫秒內(nèi)觸發(fā),并且可以確保在連接后觸發(fā)abort()。

它注釋說明的代碼是用來處理超時的,不過也從側(cè)面反映出它拋出“socket hang up”的原因:計時器啟動了,或者socket太大占內(nèi)存了。

// Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.
// And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.
// At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.
// And then these socket which be hang up will devouring CPU little by little.
// ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.

在文章開始的部分我提到兩次請求間隔較長,可以看下打印的log,可以看出第二次請求的開始,很有可能超出20s的時間限制,導(dǎo)致axois提前結(jié)束。

// 超時設(shè)置20s
firts: 19.747s
// 中間可能有多少毫秒的操作
secend: 29.197ms

總結(jié)

既然是官方還未解決的問題,一方面我們需要根據(jù)場景適當(dāng)?shù)卣{(diào)整超時設(shè)置,另一方面也要采取重試進行兜底。

原文鏈接:node發(fā)起axios請求報錯:Socket hang up

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容