Skip to content

Commit 6e280c4

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 6e280c4

File tree

1 file changed

+69
-10
lines changed

1 file changed

+69
-10
lines changed

lib/common/http-client.ts

+69-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: NodeJS.Timer;
101+
let hasResponse = false;
102+
const timers: number[] = [];
76103

77104
const promiseActions: IPromiseActions<Server.IResponse> = {
78105
resolve,
@@ -82,8 +109,9 @@ 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);
114+
timers.push(timerId);
87115

88116
delete options.timeout;
89117
}
@@ -95,6 +123,16 @@ export class HttpClient implements Server.IHttpClient {
95123
this.$logger.trace("httpRequest: %s", util.inspect(options));
96124
const requestObj = request(options);
97125

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+
98136
requestObj
99137
.on("error", (err: IHttpRequestError) => {
100138
this.$logger.trace("An error occurred while sending the request:", err);
@@ -107,15 +145,29 @@ export class HttpClient implements Server.IHttpClient {
107145
const errorMessage = this.getErrorMessage(errorMessageStatusCode, null);
108146
err.proxyAuthenticationRequired = errorMessageStatusCode === HttpStatusCodes.PROXY_AUTHENTICATION_REQUIRED;
109147
err.message = errorMessage || err.message;
110-
this.setResponseResult(promiseActions, timerId, { err });
148+
this.setResponseResult(promiseActions, timers, stuckResponseIntervalId, { err });
111149
})
112150
.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);
113162
const successful = helpers.isRequestSuccessful(response);
114163
if (!successful) {
115164
pipeTo = undefined;
116165
}
117166

118167
let responseStream = response;
168+
responseStream.on("data", (chunk: string) => {
169+
lastChunkTimestamp = Date.now();
170+
});
119171
switch (response.headers["content-encoding"]) {
120172
case "gzip":
121173
responseStream = responseStream.pipe(zlib.createGunzip());
@@ -128,7 +180,7 @@ export class HttpClient implements Server.IHttpClient {
128180
if (pipeTo) {
129181
pipeTo.on("finish", () => {
130182
this.$logger.trace("httpRequest: Piping done. code = %d", response.statusCode.toString());
131-
this.setResponseResult(promiseActions, timerId, { response });
183+
this.setResponseResult(promiseActions, timers, stuckResponseIntervalId, { response });
132184
});
133185

134186
responseStream.pipe(pipeTo);
@@ -144,13 +196,13 @@ export class HttpClient implements Server.IHttpClient {
144196
const responseBody = data.join("");
145197

146198
if (successful) {
147-
this.setResponseResult(promiseActions, timerId, { body: responseBody, response });
199+
this.setResponseResult(promiseActions, timers, stuckResponseIntervalId, { body: responseBody, response });
148200
} else {
149201
const errorMessage = this.getErrorMessage(response.statusCode, responseBody);
150202
const err: any = new Error(errorMessage);
151203
err.response = response;
152204
err.body = responseBody;
153-
this.setResponseResult(promiseActions, timerId, { err });
205+
this.setResponseResult(promiseActions, timers, stuckResponseIntervalId, { err });
154206
}
155207
});
156208
}
@@ -181,10 +233,17 @@ export class HttpClient implements Server.IHttpClient {
181233
return response;
182234
}
183235

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;
188247
}
189248

190249
if (!result.isResolved()) {

0 commit comments

Comments
 (0)