1
- import * as url from "url " ;
1
+ import * as _ from "lodash " ;
2
2
import { EOL } from "os" ;
3
- import { TLSSocket } from "tls" ;
4
- import * as helpers from "./helpers" ;
5
- import * as zlib from "zlib" ;
6
3
import * as util from "util" ;
7
- import * as _ from "lodash" ;
8
- import { HttpStatusCodes } from "./constants" ;
9
- import * as request from "request" ;
10
- import {
11
- Server ,
12
- IProxyService ,
13
- IProxySettings ,
14
- IPromiseActions ,
15
- } from "./declarations" ;
4
+ import { Server , IProxySettings , IProxyService } from "./declarations" ;
16
5
import { injector } from "./yok" ;
6
+ import axios from "axios" ;
7
+ import { HttpStatusCodes } from "./constants" ;
8
+ import * as tunnel from "tunnel" ;
17
9
18
10
export class HttpClient implements Server . IHttpClient {
19
- private static STATUS_CODE_REGEX = / s t a t u s c o d e = ( \d + ) / i;
20
11
private static STUCK_REQUEST_ERROR_MESSAGE =
21
12
"The request can't receive any response." ;
22
13
private static STUCK_RESPONSE_ERROR_MESSAGE =
23
14
"Can't receive all parts of the response." ;
24
- private static STUCK_REQUEST_TIMEOUT = 60000 ;
25
- // We receive multiple response packets every ms but we don't need to be very aggressive here.
26
- private static STUCK_RESPONSE_CHECK_INTERVAL = 10000 ;
27
15
28
16
private defaultUserAgent : string ;
29
17
@@ -39,7 +27,11 @@ export class HttpClient implements Server.IHttpClient {
39
27
) : Promise < Server . IResponse > {
40
28
try {
41
29
const result = await this . httpRequestCore ( options , proxySettings ) ;
42
- return result ;
30
+ return {
31
+ response : result ,
32
+ body : result . body ,
33
+ headers : result . headers ,
34
+ } ;
43
35
} catch ( err ) {
44
36
if (
45
37
err . message === HttpClient . STUCK_REQUEST_ERROR_MESSAGE ||
@@ -54,7 +46,11 @@ export class HttpClient implements Server.IHttpClient {
54
46
options . url || options
55
47
) ;
56
48
const retryResult = await this . httpRequestCore ( options , proxySettings ) ;
57
- return retryResult ;
49
+ return {
50
+ response : retryResult ,
51
+ body : retryResult . body ,
52
+ headers : retryResult . headers ,
53
+ } ;
58
54
}
59
55
60
56
throw err ;
@@ -72,46 +68,20 @@ export class HttpClient implements Server.IHttpClient {
72
68
} ;
73
69
}
74
70
75
- const clonedOptions = _ . cloneDeep ( options ) ;
76
-
77
- if ( clonedOptions . url ) {
78
- const urlParts = url . parse ( clonedOptions . url ) ;
79
- if ( urlParts . protocol ) {
80
- clonedOptions . proto = urlParts . protocol . slice ( 0 , - 1 ) ;
81
- }
82
- clonedOptions . host = urlParts . hostname ;
83
- clonedOptions . port = urlParts . port ;
84
- clonedOptions . path = urlParts . path ;
85
- }
86
-
87
- const requestProto = clonedOptions . proto || "http" ;
88
- const body = clonedOptions . body ;
89
- delete clonedOptions . body ;
90
- let pipeTo = options . pipeTo ; // Use the real stream because the _.cloneDeep can't clone the internal state of a stream.
91
- delete clonedOptions . pipeTo ;
92
-
93
71
const cliProxySettings = await this . $proxyService . getCache ( ) ;
72
+ const requestProto = options . proto || "http" ;
94
73
95
- clonedOptions . headers = clonedOptions . headers || { } ;
96
- const headers = clonedOptions . headers ;
74
+ options . headers = options . headers || { } ;
75
+ const headers = options . headers ;
97
76
98
77
await this . useProxySettings (
99
78
proxySettings ,
100
79
cliProxySettings ,
101
- clonedOptions ,
80
+ options ,
102
81
headers ,
103
82
requestProto
104
83
) ;
105
84
106
- if ( ! headers . Accept || headers . Accept . indexOf ( "application/json" ) < 0 ) {
107
- if ( headers . Accept ) {
108
- headers . Accept += ", " ;
109
- } else {
110
- headers . Accept = "" ;
111
- }
112
- headers . Accept += "application/json; charset=UTF-8, */*;q=0.8" ;
113
- }
114
-
115
85
if ( ! headers [ "User-Agent" ] ) {
116
86
if ( ! this . defaultUserAgent ) {
117
87
//TODO: the user agent client name is also passed explicitly during login and should be kept in sync
@@ -126,208 +96,51 @@ export class HttpClient implements Server.IHttpClient {
126
96
headers [ "Accept-Encoding" ] = "gzip,deflate" ;
127
97
}
128
98
129
- const result = new Promise < Server . IResponse > ( ( resolve , reject ) => {
130
- let timerId : NodeJS . Timer ;
131
- const cleanupRequestData : ICleanupRequestData = Object . create ( {
132
- timers : [ ] ,
133
- } ) ;
134
-
135
- const promiseActions : IPromiseActions < Server . IResponse > = {
136
- resolve,
137
- reject,
138
- isResolved : ( ) => false ,
139
- } ;
140
-
141
- clonedOptions . url =
142
- clonedOptions . url ||
143
- `${ clonedOptions . proto } ://${ clonedOptions . host } ${ clonedOptions . path } ` ;
144
- if ( clonedOptions . timeout ) {
145
- timerId = setTimeout ( ( ) => {
146
- this . setResponseResult ( promiseActions , cleanupRequestData , {
147
- err : new Error ( `Request to ${ clonedOptions . url } timed out.` ) ,
148
- } ) ;
149
- } , clonedOptions . timeout ) ;
150
- cleanupRequestData . timers . push ( timerId ) ;
151
-
152
- delete clonedOptions . timeout ;
153
- }
154
-
155
- clonedOptions . encoding = null ;
156
- clonedOptions . followAllRedirects = true ;
157
-
158
- this . $logger . trace ( "httpRequest: %s" , util . inspect ( clonedOptions ) ) ;
159
- const requestObj = request ( clonedOptions ) ;
160
- cleanupRequestData . req = requestObj ;
161
-
162
- requestObj
163
- . on ( "error" , ( err : any ) => {
164
- this . $logger . trace (
165
- "An error occurred while sending the request:" ,
166
- err
167
- ) ;
168
- // In case we get a 4xx error code there seems to be no better way than this regex to get the error code
169
- // the tunnel-agent module that request is using is obscuring the response and hence the statusCode by throwing an error message
170
- // https://github.com/request/tunnel-agent/blob/eb2b1b19e09ee0e6a2b54eb2612755731b7301dc/index.js#L166
171
- // in case there is a better way to obtain status code in future version do not hesitate to remove this code
172
- const errorMessageMatch = err . message . match (
173
- HttpClient . STATUS_CODE_REGEX
174
- ) ;
175
- const errorMessageStatusCode =
176
- errorMessageMatch && errorMessageMatch [ 1 ] && + errorMessageMatch [ 1 ] ;
177
- const errorMessage = this . getErrorMessage (
178
- errorMessageStatusCode ,
179
- null
180
- ) ;
181
- err . proxyAuthenticationRequired =
182
- errorMessageStatusCode ===
183
- HttpStatusCodes . PROXY_AUTHENTICATION_REQUIRED ;
184
- err . message = errorMessage || err . message ;
185
- this . setResponseResult ( promiseActions , cleanupRequestData , { err } ) ;
186
- } )
187
- . on ( "socket" , ( s : TLSSocket ) => {
188
- let stuckRequestTimerId : NodeJS . Timer ;
189
-
190
- stuckRequestTimerId = setTimeout ( ( ) => {
191
- this . setResponseResult ( promiseActions , cleanupRequestData , {
192
- err : new Error ( HttpClient . STUCK_REQUEST_ERROR_MESSAGE ) ,
193
- } ) ;
194
- } , clonedOptions . timeout || HttpClient . STUCK_REQUEST_TIMEOUT ) ;
195
-
196
- cleanupRequestData . timers . push ( stuckRequestTimerId ) ;
197
-
198
- s . once ( "secureConnect" , ( ) => {
199
- clearTimeout ( stuckRequestTimerId ) ;
200
- stuckRequestTimerId = null ;
201
- } ) ;
202
- } )
203
- . on ( "response" , ( responseData : Server . IRequestResponseData ) => {
204
- cleanupRequestData . res = responseData ;
205
- let lastChunkTimestamp = Date . now ( ) ;
206
- cleanupRequestData . stuckResponseIntervalId = setInterval ( ( ) => {
207
- if (
208
- Date . now ( ) - lastChunkTimestamp >
209
- HttpClient . STUCK_RESPONSE_CHECK_INTERVAL
210
- ) {
211
- this . setResponseResult ( promiseActions , cleanupRequestData , {
212
- err : new Error ( HttpClient . STUCK_RESPONSE_ERROR_MESSAGE ) ,
213
- } ) ;
214
- }
215
- } , HttpClient . STUCK_RESPONSE_CHECK_INTERVAL ) ;
216
- const successful =
217
- helpers . isRequestSuccessful ( responseData ) ||
218
- responseData . statusCode === HttpStatusCodes . NOT_MODIFIED ;
219
- if ( ! successful ) {
220
- pipeTo = undefined ;
221
- }
222
-
223
- let responseStream : any = responseData ;
224
- responseStream . on ( "data" , ( chunk : string ) => {
225
- lastChunkTimestamp = Date . now ( ) ;
226
- } ) ;
227
- switch ( responseData . headers [ "content-encoding" ] ) {
228
- case "gzip" :
229
- responseStream = responseStream . pipe ( zlib . createGunzip ( ) ) ;
230
- break ;
231
- case "deflate" :
232
- responseStream = responseStream . pipe ( zlib . createInflate ( ) ) ;
233
- break ;
234
- }
235
-
236
- if ( pipeTo ) {
237
- pipeTo . on ( "finish" , ( ) => {
238
- this . $logger . trace (
239
- "httpRequest: Piping done. code = %d" ,
240
- responseData . statusCode . toString ( )
241
- ) ;
242
- this . setResponseResult ( promiseActions , cleanupRequestData , {
243
- response : responseData ,
244
- } ) ;
245
- } ) ;
246
-
247
- responseStream . pipe ( pipeTo ) ;
248
- } else {
249
- const data : string [ ] = [ ] ;
250
-
251
- responseStream . on ( "data" , ( chunk : string ) => {
252
- data . push ( chunk ) ;
253
- } ) ;
254
-
255
- responseStream . on ( "end" , ( ) => {
256
- this . $logger . trace (
257
- "httpRequest: Done. code = %d" ,
258
- responseData . statusCode . toString ( )
259
- ) ;
260
- const responseBody = data . join ( "" ) ;
261
-
262
- if ( successful ) {
263
- this . setResponseResult ( promiseActions , cleanupRequestData , {
264
- body : responseBody ,
265
- response : responseData ,
266
- } ) ;
267
- } else {
268
- const errorMessage = this . getErrorMessage (
269
- responseData . statusCode ,
270
- responseBody
271
- ) ;
272
- const err : any = new Error ( errorMessage ) ;
273
- err . response = responseData ;
274
- err . body = responseBody ;
275
- this . setResponseResult ( promiseActions , cleanupRequestData , {
276
- err,
277
- } ) ;
278
- }
279
- } ) ;
280
- }
281
- } ) ;
99
+ this . $logger . trace ( "httpRequest: %s" , util . inspect ( options ) ) ;
282
100
283
- this . $logger . trace (
284
- "httpRequest: Sending:\n%s" ,
285
- this . $logger . prepare ( body )
286
- ) ;
287
-
288
- if ( ! body || ! body . pipe ) {
289
- requestObj . end ( body ) ;
101
+ const agent = tunnel . httpsOverHttp ( {
102
+ proxy : {
103
+ host : cliProxySettings . hostname ,
104
+ port : parseInt ( cliProxySettings . port ) ,
105
+ } ,
106
+ } ) ;
107
+ const result = await axios ( {
108
+ url : options . url ,
109
+ headers : options . headers ,
110
+ method : options . method ,
111
+ proxy : false ,
112
+ httpAgent : agent ,
113
+ } ) . catch ( ( err ) => {
114
+ this . $logger . trace ( "An error occurred while sending the request:" , err ) ;
115
+ if ( err . response ) {
116
+ // The request was made and the server responded with a status code
117
+ // that falls out of the range of 2xx
118
+ const errorMessage = this . getErrorMessage ( err . response . status , null ) ;
119
+
120
+ err . proxyAuthenticationRequired =
121
+ err . response . status === HttpStatusCodes . PROXY_AUTHENTICATION_REQUIRED ;
122
+
123
+ err . message = errorMessage || err . message ;
124
+ } else if ( err . request ) {
125
+ // The request was made but no response was received
126
+ // `err.request` is an instance of XMLHttpRequest in the browser and an instance of
290
127
} else {
291
- body . pipe ( requestObj ) ;
128
+ // Something happened in setting up the request that triggered an Error
292
129
}
130
+ throw err ;
293
131
} ) ;
294
132
295
- const response = await result ;
296
-
297
- if ( helpers . isResponseRedirect ( response . response ) ) {
298
- const unmodifiedOptions = _ . cloneDeep ( options ) ;
299
- if ( response . response . statusCode === HttpStatusCodes . SEE_OTHER ) {
300
- unmodifiedOptions . method = "GET" ;
301
- }
302
-
303
- this . $logger . trace ( "Begin redirected to %s" , response . headers . location ) ;
304
- unmodifiedOptions . url = response . headers . location ;
305
- return await this . httpRequest ( unmodifiedOptions ) ;
306
- }
307
-
308
- return response ;
309
- }
310
-
311
- private setResponseResult (
312
- result : IPromiseActions < Server . IResponse > ,
313
- cleanupRequestData : ICleanupRequestData ,
314
- resultData : {
315
- response ?: Server . IRequestResponseData ;
316
- body ?: string ;
317
- err ?: Error ;
318
- }
319
- ) : void {
320
- this . cleanupAfterRequest ( cleanupRequestData ) ;
321
- if ( ! result . isResolved ( ) ) {
322
- result . isResolved = ( ) => true ;
323
- if ( resultData . err || ! resultData . response . complete ) {
324
- return result . reject ( resultData . err || new Error ( "Request canceled" ) ) ;
325
- }
326
-
327
- const finalResult : any = resultData ;
328
- finalResult . headers = resultData . response . headers ;
133
+ if ( result ) {
134
+ this . $logger . trace (
135
+ "httpRequest: Done. code = %d" ,
136
+ result . status . toString ( )
137
+ ) ;
329
138
330
- result . resolve ( finalResult ) ;
139
+ return {
140
+ response : result ,
141
+ body : JSON . stringify ( result . data ) ,
142
+ headers : result . headers ,
143
+ } ;
331
144
}
332
145
}
333
146
@@ -410,35 +223,5 @@ export class HttpClient implements Server.IHttpClient {
410
223
this . $logger . trace ( "Using proxy: %s" , options . proxy ) ;
411
224
}
412
225
}
413
-
414
- private cleanupAfterRequest ( data : ICleanupRequestData ) : void {
415
- data . timers . forEach ( ( t ) => {
416
- if ( t ) {
417
- clearTimeout ( t ) ;
418
- t = null ;
419
- }
420
- } ) ;
421
-
422
- if ( data . stuckResponseIntervalId ) {
423
- clearInterval ( data . stuckResponseIntervalId ) ;
424
- data . stuckResponseIntervalId = null ;
425
- }
426
-
427
- if ( data . req ) {
428
- data . req . abort ( ) ;
429
- }
430
-
431
- if ( data . res ) {
432
- data . res . destroy ( ) ;
433
- }
434
- }
435
226
}
436
-
437
- interface ICleanupRequestData {
438
- timers : NodeJS . Timer [ ] ;
439
- stuckResponseIntervalId : NodeJS . Timer ;
440
- req : request . Request ;
441
- res : Server . IRequestResponseData ;
442
- }
443
-
444
227
injector . register ( "httpClient" , HttpClient ) ;
0 commit comments