1- import * as url from "url " ;
1+ import * as _ from "lodash " ;
22import { EOL } from "os" ;
3- import { TLSSocket } from "tls" ;
4- import * as helpers from "./helpers" ;
5- import * as zlib from "zlib" ;
63import * 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" ;
165import { injector } from "./yok" ;
6+ import axios from "axios" ;
7+ import { HttpStatusCodes } from "./constants" ;
8+ import * as tunnel from "tunnel" ;
179
1810export class HttpClient implements Server . IHttpClient {
19- private static STATUS_CODE_REGEX = / s t a t u s c o d e = ( \d + ) / i;
2011 private static STUCK_REQUEST_ERROR_MESSAGE =
2112 "The request can't receive any response." ;
2213 private static STUCK_RESPONSE_ERROR_MESSAGE =
2314 "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 ;
2715
2816 private defaultUserAgent : string ;
2917
@@ -39,7 +27,11 @@ export class HttpClient implements Server.IHttpClient {
3927 ) : Promise < Server . IResponse > {
4028 try {
4129 const result = await this . httpRequestCore ( options , proxySettings ) ;
42- return result ;
30+ return {
31+ response : result ,
32+ body : result . body ,
33+ headers : result . headers ,
34+ } ;
4335 } catch ( err ) {
4436 if (
4537 err . message === HttpClient . STUCK_REQUEST_ERROR_MESSAGE ||
@@ -54,7 +46,11 @@ export class HttpClient implements Server.IHttpClient {
5446 options . url || options
5547 ) ;
5648 const retryResult = await this . httpRequestCore ( options , proxySettings ) ;
57- return retryResult ;
49+ return {
50+ response : retryResult ,
51+ body : retryResult . body ,
52+ headers : retryResult . headers ,
53+ } ;
5854 }
5955
6056 throw err ;
@@ -72,46 +68,20 @@ export class HttpClient implements Server.IHttpClient {
7268 } ;
7369 }
7470
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-
9371 const cliProxySettings = await this . $proxyService . getCache ( ) ;
72+ const requestProto = options . proto || "http" ;
9473
95- clonedOptions . headers = clonedOptions . headers || { } ;
96- const headers = clonedOptions . headers ;
74+ options . headers = options . headers || { } ;
75+ const headers = options . headers ;
9776
9877 await this . useProxySettings (
9978 proxySettings ,
10079 cliProxySettings ,
101- clonedOptions ,
80+ options ,
10281 headers ,
10382 requestProto
10483 ) ;
10584
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-
11585 if ( ! headers [ "User-Agent" ] ) {
11686 if ( ! this . defaultUserAgent ) {
11787 //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 {
12696 headers [ "Accept-Encoding" ] = "gzip,deflate" ;
12797 }
12898
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 ) ) ;
282100
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
290127 } else {
291- body . pipe ( requestObj ) ;
128+ // Something happened in setting up the request that triggered an Error
292129 }
130+ throw err ;
293131 } ) ;
294132
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+ ) ;
329138
330- result . resolve ( finalResult ) ;
139+ return {
140+ response : result ,
141+ body : JSON . stringify ( result . data ) ,
142+ headers : result . headers ,
143+ } ;
331144 }
332145 }
333146
@@ -410,35 +223,5 @@ export class HttpClient implements Server.IHttpClient {
410223 this . $logger . trace ( "Using proxy: %s" , options . proxy ) ;
411224 }
412225 }
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- }
435226}
436-
437- interface ICleanupRequestData {
438- timers : NodeJS . Timer [ ] ;
439- stuckResponseIntervalId : NodeJS . Timer ;
440- req : request . Request ;
441- res : Server . IRequestResponseData ;
442- }
443-
444227injector . register ( "httpClient" , HttpClient ) ;
0 commit comments