Skip to content

Commit 517e082

Browse files
refactor: axios http client instead of request (#5473)
* refactor: axios http client instead of request * refactor: axios proxy settings * Update yarn.lock Co-authored-by: rigor789 <[email protected]>
1 parent bfa8e91 commit 517e082

File tree

3 files changed

+7355
-7791
lines changed

3 files changed

+7355
-7791
lines changed

lib/common/http-client.ts

+58-275
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
1-
import * as url from "url";
1+
import * as _ from "lodash";
22
import { EOL } from "os";
3-
import { TLSSocket } from "tls";
4-
import * as helpers from "./helpers";
5-
import * as zlib from "zlib";
63
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";
165
import { injector } from "./yok";
6+
import axios from "axios";
7+
import { HttpStatusCodes } from "./constants";
8+
import * as tunnel from "tunnel";
179

1810
export class HttpClient implements Server.IHttpClient {
19-
private static STATUS_CODE_REGEX = /statuscode=(\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-
444227
injector.register("httpClient", HttpClient);

0 commit comments

Comments
 (0)