@@ -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 : NodeJS . Timer ;
101
+ let hasResponse = false ;
102
+ const timers : number [ ] = [ ] ;
76
103
77
104
const promiseActions : IPromiseActions < Server . IResponse > = {
78
105
resolve,
@@ -82,8 +109,9 @@ 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 ) ;
114
+ timers . push ( timerId ) ;
87
115
88
116
delete options . timeout ;
89
117
}
@@ -95,6 +123,16 @@ export class HttpClient implements Server.IHttpClient {
95
123
this . $logger . trace ( "httpRequest: %s" , util . inspect ( options ) ) ;
96
124
const requestObj = request ( options ) ;
97
125
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
+
98
136
requestObj
99
137
. on ( "error" , ( err : IHttpRequestError ) => {
100
138
this . $logger . trace ( "An error occurred while sending the request:" , err ) ;
@@ -107,15 +145,29 @@ export class HttpClient implements Server.IHttpClient {
107
145
const errorMessage = this . getErrorMessage ( errorMessageStatusCode , null ) ;
108
146
err . proxyAuthenticationRequired = errorMessageStatusCode === HttpStatusCodes . PROXY_AUTHENTICATION_REQUIRED ;
109
147
err . message = errorMessage || err . message ;
110
- this . setResponseResult ( promiseActions , timerId , { err } ) ;
148
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err } ) ;
111
149
} )
112
150
. 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 ) ;
113
162
const successful = helpers . isRequestSuccessful ( response ) ;
114
163
if ( ! successful ) {
115
164
pipeTo = undefined ;
116
165
}
117
166
118
167
let responseStream = response ;
168
+ responseStream . on ( "data" , ( chunk : string ) => {
169
+ lastChunkTimestamp = Date . now ( ) ;
170
+ } ) ;
119
171
switch ( response . headers [ "content-encoding" ] ) {
120
172
case "gzip" :
121
173
responseStream = responseStream . pipe ( zlib . createGunzip ( ) ) ;
@@ -128,7 +180,7 @@ export class HttpClient implements Server.IHttpClient {
128
180
if ( pipeTo ) {
129
181
pipeTo . on ( "finish" , ( ) => {
130
182
this . $logger . trace ( "httpRequest: Piping done. code = %d" , response . statusCode . toString ( ) ) ;
131
- this . setResponseResult ( promiseActions , timerId , { response } ) ;
183
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { response } ) ;
132
184
} ) ;
133
185
134
186
responseStream . pipe ( pipeTo ) ;
@@ -144,13 +196,13 @@ export class HttpClient implements Server.IHttpClient {
144
196
const responseBody = data . join ( "" ) ;
145
197
146
198
if ( successful ) {
147
- this . setResponseResult ( promiseActions , timerId , { body : responseBody , response } ) ;
199
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { body : responseBody , response } ) ;
148
200
} else {
149
201
const errorMessage = this . getErrorMessage ( response . statusCode , responseBody ) ;
150
202
const err : any = new Error ( errorMessage ) ;
151
203
err . response = response ;
152
204
err . body = responseBody ;
153
- this . setResponseResult ( promiseActions , timerId , { err } ) ;
205
+ this . setResponseResult ( promiseActions , timers , stuckResponseIntervalId , { err } ) ;
154
206
}
155
207
} ) ;
156
208
}
@@ -181,10 +233,17 @@ export class HttpClient implements Server.IHttpClient {
181
233
return response ;
182
234
}
183
235
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 ;
188
247
}
189
248
190
249
if ( ! result . isResolved ( ) ) {
0 commit comments