@@ -9,13 +9,36 @@ import * as request from "request";
99export class HttpClient implements Server . IHttpClient {
1010private defaultUserAgent : string ;
1111private static STATUS_CODE_REGEX = / s t a t u s c o d e = ( \d + ) / i;
12+ private static STUCK_REQUEST_ERROR_MESSAGE = "The request can't receive any response." ;
13+ private static STUCK_RESPONSE_ERROR_MESSAGE = "Can't receive all parts of the response." ;
14+ private static STUCK_REQUEST_TIMEOUT = 60000 ;
15+ // We receive multiple response packets every ms but we don't need to be very aggressive here.
16+ private static STUCK_RESPONSE_CHECK_INTERVAL = 10000 ;
1217
1318constructor ( private $config : Config . IConfig ,
1419private $logger : ILogger ,
1520private $proxyService : IProxyService ,
1621private $staticConfig : Config . IStaticConfig ) { }
1722
18- async httpRequest ( options : any , proxySettings ?: IProxySettings ) : Promise < Server . IResponse > {
23+ public async httpRequest ( options : any , proxySettings ?: IProxySettings ) : Promise < Server . IResponse > {
24+ try {
25+ const result = await this . httpRequestCore ( options , proxySettings ) ;
26+ return result ;
27+ } catch ( err ) {
28+ if ( err . message === HttpClient . STUCK_REQUEST_ERROR_MESSAGE || err . message === HttpClient . STUCK_RESPONSE_ERROR_MESSAGE ) {
29+ // Retry the request immediately because there are at least 10 seconds between the two requests.
30+ // We have to retry only once the sporadically stuck requests/responses.
31+ // We can add exponential backoff retry here if we decide that we need to workaround bigger network issues on the client side.
32+ this . $logger . warn ( "%s Retrying request to %s..." , err . message , options . url || options ) ;
33+ const retryResult = await this . httpRequestCore ( options , proxySettings ) ;
34+ return retryResult ;
35+ }
36+
37+ throw err ;
38+ }
39+ }
40+
41+ private async httpRequestCore ( options : any , proxySettings ?: IProxySettings ) : Promise < Server . IResponse > {
1942if ( _ . isString ( options ) ) {
2043options = {
2144url : options ,
@@ -73,6 +96,10 @@ export class HttpClient implements Server.IHttpClient {
7396
7497const result = new Promise < Server . IResponse > ( ( resolve , reject ) => {
7598let timerId : number ;
99+ let stuckRequestTimerId : number ;
100+ let stuckResponseIntervalId : NodeJS . Timer ;
101+ let hasResponse = false ;
102+ const timers : number [ ] = [ ] ;
76103
77104const promiseActions : IPromiseActions < Server . IResponse > = {
78105resolve,
@@ -82,8 +109,9 @@ export class HttpClient implements Server.IHttpClient {
82109
83110if ( options . timeout ) {
84111timerId = setTimeout ( ( ) => {
85- this . setResponseResult ( promiseActions , timerId , { err : new Error ( `Request to ${ unmodifiedOptions . url } timed out.` ) } , ) ;
112+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err : new Error ( `Request to ${ unmodifiedOptions . url } timed out.` ) } ) ;
86113} , options . timeout ) ;
114+ timers . push ( timerId ) ;
87115
88116delete options . timeout ;
89117}
@@ -95,6 +123,16 @@ export class HttpClient implements Server.IHttpClient {
95123this . $logger . trace ( "httpRequest: %s" , util . inspect ( options ) ) ;
96124const requestObj = request ( options ) ;
97125
126+ stuckRequestTimerId = setTimeout ( ( ) => {
127+ clearTimeout ( stuckRequestTimerId ) ;
128+ stuckRequestTimerId = null ;
129+ if ( ! hasResponse ) {
130+ requestObj . abort ( ) ;
131+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err : new Error ( HttpClient . STUCK_REQUEST_ERROR_MESSAGE ) } ) ;
132+ }
133+ } , options . timeout || HttpClient . STUCK_REQUEST_TIMEOUT ) ;
134+ timers . push ( stuckRequestTimerId ) ;
135+
98136requestObj
99137. on ( "error" , ( err : IHttpRequestError ) => {
100138this . $logger . trace ( "An error occurred while sending the request:" , err ) ;
@@ -107,15 +145,29 @@ export class HttpClient implements Server.IHttpClient {
107145const errorMessage = this . getErrorMessage ( errorMessageStatusCode , null ) ;
108146err . proxyAuthenticationRequired = errorMessageStatusCode === HttpStatusCodes . PROXY_AUTHENTICATION_REQUIRED ;
109147err . message = errorMessage || err . message ;
110- this . setResponseResult ( promiseActions , timerId , { err } ) ;
148+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err } ) ;
111149} )
112150. on ( "response" , ( response : Server . IRequestResponseData ) => {
151+ hasResponse = true ;
152+ let lastChunkTimestamp = Date . now ( ) ;
153+ stuckResponseIntervalId = setInterval ( ( ) => {
154+ if ( Date . now ( ) - lastChunkTimestamp > HttpClient . STUCK_RESPONSE_CHECK_INTERVAL ) {
155+ if ( ( < any > response ) . destroy ) {
156+ ( < any > response ) . destroy ( ) ;
157+ }
158+
159+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err : new Error ( HttpClient . STUCK_RESPONSE_ERROR_MESSAGE ) } ) ;
160+ }
161+ } , HttpClient . STUCK_RESPONSE_CHECK_INTERVAL ) ;
113162const successful = helpers . isRequestSuccessful ( response ) ;
114163if ( ! successful ) {
115164pipeTo = undefined ;
116165}
117166
118167let responseStream = response ;
168+ responseStream . on ( "data" , ( chunk : string ) => {
169+ lastChunkTimestamp = Date . now ( ) ;
170+ } ) ;
119171switch ( response . headers [ "content-encoding" ] ) {
120172case "gzip" :
121173responseStream = responseStream . pipe ( zlib . createGunzip ( ) ) ;
@@ -128,7 +180,7 @@ export class HttpClient implements Server.IHttpClient {
128180if ( pipeTo ) {
129181pipeTo . on ( "finish" , ( ) => {
130182this . $logger . trace ( "httpRequest: Piping done. code = %d" , response . statusCode . toString ( ) ) ;
131- this . setResponseResult ( promiseActions , timerId , { response } ) ;
183+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { response } ) ;
132184} ) ;
133185
134186responseStream . pipe ( pipeTo ) ;
@@ -144,13 +196,13 @@ export class HttpClient implements Server.IHttpClient {
144196const responseBody = data . join ( "" ) ;
145197
146198if ( successful ) {
147- this . setResponseResult ( promiseActions , timerId , { body : responseBody , response } ) ;
199+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { body : responseBody , response } ) ;
148200} else {
149201const errorMessage = this . getErrorMessage ( response . statusCode , responseBody ) ;
150202const err : any = new Error ( errorMessage ) ;
151203err . response = response ;
152204err . body = responseBody ;
153- this . setResponseResult ( promiseActions , timerId , { err } ) ;
205+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err } ) ;
154206}
155207} ) ;
156208}
@@ -181,10 +233,17 @@ export class HttpClient implements Server.IHttpClient {
181233return response ;
182234}
183235
184- private setResponseResult ( result : IPromiseActions < Server . IResponse > , timerId : number , resultData : { response ?: Server . IRequestResponseData , body ?: string , err ?: Error } ) : void {
185- if ( timerId ) {
186- clearTimeout ( timerId ) ;
187- timerId = null ;
236+ private setResponseResult ( result : IPromiseActions < Server . IResponse > , timers : number [ ] , stuckResponseIntervalId : NodeJS . Timer , resultData : { response ?: Server . IRequestResponseData , body ?: string , err ?: Error } ) : void {
237+ timers . forEach ( t => {
238+ if ( t ) {
239+ clearTimeout ( t ) ;
240+ t = null ;
241+ }
242+ } ) ;
243+
244+ if ( stuckResponseIntervalId ) {
245+ clearInterval ( stuckResponseIntervalId ) ;
246+ stuckResponseIntervalId = null ;
188247}
189248
190249if ( ! result . isResolved ( ) ) {
0 commit comments