@@ -9,13 +9,36 @@ import * as request from "request";
9
9
export class HttpClient implements Server . IHttpClient {
10
10
private defaultUserAgent : string ;
11
11
private 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 ;
12
17
13
18
constructor ( private $config : Config . IConfig ,
14
19
private $logger : ILogger ,
15
20
private $proxyService : IProxyService ,
16
21
private $staticConfig : Config . IStaticConfig ) { }
17
22
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 > {
19
42
if ( _ . isString ( options ) ) {
20
43
options = {
21
44
url : options ,
@@ -73,6 +96,10 @@ export class HttpClient implements Server.IHttpClient {
73
96
74
97
const result = new Promise < Server . IResponse > ( ( resolve , reject ) => {
75
98
let timerId : number ;
99
+ let stuckRequestTimerId : number ;
100
+ let stuckResponseIntervalId : number ;
101
+ const timers = [ timerId , stuckRequestTimerId ] ;
102
+ let hasResponse = false ;
76
103
77
104
const promiseActions : IPromiseActions < Server . IResponse > = {
78
105
resolve,
@@ -82,7 +109,7 @@ export class HttpClient implements Server.IHttpClient {
82
109
83
110
if ( options . timeout ) {
84
111
timerId = 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.` ) } ) ;
86
113
} , options . timeout ) ;
87
114
88
115
delete options . timeout ;
@@ -95,6 +122,15 @@ export class HttpClient implements Server.IHttpClient {
95
122
this . $logger . trace ( "httpRequest: %s" , util . inspect ( options ) ) ;
96
123
const requestObj = request ( options ) ;
97
124
125
+ stuckRequestTimerId = setTimeout ( ( ) => {
126
+ clearTimeout ( stuckRequestTimerId ) ;
127
+ stuckRequestTimerId = null ;
128
+ if ( ! hasResponse ) {
129
+ requestObj . abort ( ) ;
130
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err : new Error ( HttpClient . STUCK_REQUEST_ERROR_MESSAGE ) } ) ;
131
+ }
132
+ } , options . timeout || HttpClient . STUCK_REQUEST_TIMEOUT ) ;
133
+
98
134
requestObj
99
135
. on ( "error" , ( err : IHttpRequestError ) => {
100
136
this . $logger . trace ( "An error occurred while sending the request:" , err ) ;
@@ -107,15 +143,29 @@ export class HttpClient implements Server.IHttpClient {
107
143
const errorMessage = this . getErrorMessage ( errorMessageStatusCode , null ) ;
108
144
err . proxyAuthenticationRequired = errorMessageStatusCode === HttpStatusCodes . PROXY_AUTHENTICATION_REQUIRED ;
109
145
err . message = errorMessage || err . message ;
110
- this . setResponseResult ( promiseActions , timerId , { err } ) ;
146
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err } ) ;
111
147
} )
112
148
. on ( "response" , ( response : Server . IRequestResponseData ) => {
149
+ hasResponse = true ;
150
+ let lastChunkTimestamp = Date . now ( ) ;
151
+ stuckResponseIntervalId = setInterval ( ( ) => {
152
+ if ( Date . now ( ) - lastChunkTimestamp > HttpClient . STUCK_RESPONSE_CHECK_INTERVAL ) {
153
+ if ( ( < any > response ) . destroy ) {
154
+ ( < any > response ) . destroy ( ) ;
155
+ }
156
+
157
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err : new Error ( HttpClient . STUCK_RESPONSE_ERROR_MESSAGE ) } ) ;
158
+ }
159
+ } , HttpClient . STUCK_RESPONSE_CHECK_INTERVAL ) ;
113
160
const successful = helpers . isRequestSuccessful ( response ) ;
114
161
if ( ! successful ) {
115
162
pipeTo = undefined ;
116
163
}
117
164
118
165
let responseStream = response ;
166
+ responseStream . on ( "data" , ( chunk : string ) => {
167
+ lastChunkTimestamp = Date . now ( ) ;
168
+ } ) ;
119
169
switch ( response . headers [ "content-encoding" ] ) {
120
170
case "gzip" :
121
171
responseStream = responseStream . pipe ( zlib . createGunzip ( ) ) ;
@@ -128,7 +178,7 @@ export class HttpClient implements Server.IHttpClient {
128
178
if ( pipeTo ) {
129
179
pipeTo . on ( "finish" , ( ) => {
130
180
this . $logger . trace ( "httpRequest: Piping done. code = %d" , response . statusCode . toString ( ) ) ;
131
- this . setResponseResult ( promiseActions , timerId , { response } ) ;
181
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { response } ) ;
132
182
} ) ;
133
183
134
184
responseStream . pipe ( pipeTo ) ;
@@ -144,13 +194,13 @@ export class HttpClient implements Server.IHttpClient {
144
194
const responseBody = data . join ( "" ) ;
145
195
146
196
if ( successful ) {
147
- this . setResponseResult ( promiseActions , timerId , { body : responseBody , response } ) ;
197
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { body : responseBody , response } ) ;
148
198
} else {
149
199
const errorMessage = this . getErrorMessage ( response . statusCode , responseBody ) ;
150
200
const err : any = new Error ( errorMessage ) ;
151
201
err . response = response ;
152
202
err . body = responseBody ;
153
- this . setResponseResult ( promiseActions , timerId , { err } ) ;
203
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err } ) ;
154
204
}
155
205
} ) ;
156
206
}
@@ -181,10 +231,17 @@ export class HttpClient implements Server.IHttpClient {
181
231
return response ;
182
232
}
183
233
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 ;
234
+ private setResponseResult ( result : IPromiseActions < Server . IResponse > , timers : number [ ] , stuckResponseIntervalId : number , resultData : { response ?: Server . IRequestResponseData , body ?: string , err ?: Error } ) : void {
235
+ timers . forEach ( t => {
236
+ if ( t ) {
237
+ clearTimeout ( t ) ;
238
+ t = null ;
239
+ }
240
+ } ) ;
241
+
242
+ if ( stuckResponseIntervalId ) {
243
+ clearInterval ( stuckResponseIntervalId ) ;
244
+ stuckResponseIntervalId = null ;
188
245
}
189
246
190
247
if ( ! result . isResolved ( ) ) {
0 commit comments