Skip to content

Commit 21fbfb0

Browse files
Retry sporadically stuck requests/responses
Sometimes we have sporadically stuck http requests/responses and in those cases the CLI is just waiting and not doing anything. If we retry the requests, they will pass. That's why we need to add a retry logic in the http client.
1 parent e470e25 commit 21fbfb0

File tree

1 file changed

+67
-10
lines changed

1 file changed

+67
-10
lines changed

lib/common/http-client.ts

+67-10
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,36 @@ import * as request from "request";
99
export class HttpClient implements Server.IHttpClient {
1010
private defaultUserAgent: string;
1111
private static STATUS_CODE_REGEX = /statuscode=(\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;
1217

1318
constructor(private $config: Config.IConfig,
1419
private $logger: ILogger,
1520
private $proxyService: IProxyService,
1621
private $staticConfig: Config.IStaticConfig) { }
1722

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> {
1942
if (_.isString(options)) {
2043
options = {
2144
url: options,
@@ -73,6 +96,10 @@ export class HttpClient implements Server.IHttpClient {
7396

7497
const result = new Promise<Server.IResponse>((resolve, reject) => {
7598
let timerId: number;
99+
let stuckRequestTimerId: number;
100+
let stuckResponseIntervalId: number;
101+
const timers = [timerId, stuckRequestTimerId];
102+
let hasResponse = false;
76103

77104
const promiseActions: IPromiseActions<Server.IResponse> = {
78105
resolve,
@@ -82,7 +109,7 @@ export class HttpClient implements Server.IHttpClient {
82109

83110
if (options.timeout) {
84111
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.`) });
86113
}, options.timeout);
87114

88115
delete options.timeout;
@@ -95,6 +122,15 @@ export class HttpClient implements Server.IHttpClient {
95122
this.$logger.trace("httpRequest: %s", util.inspect(options));
96123
const requestObj = request(options);
97124

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+
98134
requestObj
99135
.on("error", (err: IHttpRequestError) => {
100136
this.$logger.trace("An error occurred while sending the request:", err);
@@ -107,15 +143,29 @@ export class HttpClient implements Server.IHttpClient {
107143
const errorMessage = this.getErrorMessage(errorMessageStatusCode, null);
108144
err.proxyAuthenticationRequired = errorMessageStatusCode === HttpStatusCodes.PROXY_AUTHENTICATION_REQUIRED;
109145
err.message = errorMessage || err.message;
110-
this.setResponseResult(promiseActions, timerId, { err });
146+
this.setResponseResult(promiseActions, timers, stuckResponseIntervalId, { err });
111147
})
112148
.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);
113160
const successful = helpers.isRequestSuccessful(response);
114161
if (!successful) {
115162
pipeTo = undefined;
116163
}
117164

118165
let responseStream = response;
166+
responseStream.on("data", (chunk: string) => {
167+
lastChunkTimestamp = Date.now();
168+
});
119169
switch (response.headers["content-encoding"]) {
120170
case "gzip":
121171
responseStream = responseStream.pipe(zlib.createGunzip());
@@ -128,7 +178,7 @@ export class HttpClient implements Server.IHttpClient {
128178
if (pipeTo) {
129179
pipeTo.on("finish", () => {
130180
this.$logger.trace("httpRequest: Piping done. code = %d", response.statusCode.toString());
131-
this.setResponseResult(promiseActions, timerId, { response });
181+
this.setResponseResult(promiseActions, timers, stuckResponseIntervalId, { response });
132182
});
133183

134184
responseStream.pipe(pipeTo);
@@ -144,13 +194,13 @@ export class HttpClient implements Server.IHttpClient {
144194
const responseBody = data.join("");
145195

146196
if (successful) {
147-
this.setResponseResult(promiseActions, timerId, { body: responseBody, response });
197+
this.setResponseResult(promiseActions, timers, stuckResponseIntervalId, { body: responseBody, response });
148198
} else {
149199
const errorMessage = this.getErrorMessage(response.statusCode, responseBody);
150200
const err: any = new Error(errorMessage);
151201
err.response = response;
152202
err.body = responseBody;
153-
this.setResponseResult(promiseActions, timerId, { err });
203+
this.setResponseResult(promiseActions, timers, stuckResponseIntervalId, { err });
154204
}
155205
});
156206
}
@@ -181,10 +231,17 @@ export class HttpClient implements Server.IHttpClient {
181231
return response;
182232
}
183233

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;
188245
}
189246

190247
if (!result.isResolved()) {

0 commit comments

Comments
 (0)