Skip to content

Commit d34a99d

Browse files
authored
Merge pull request #87 from wolfpackthatcodes/feature/add-retry-mechanism
Add support for retry mechanism
2 parents a02ad17 + 40c2cc9 commit d34a99d

File tree

4 files changed

+260
-87
lines changed

4 files changed

+260
-87
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,26 @@ const response = new HttpClient()
299299
</details>
300300

301301
<details>
302-
<summary><b>6. Testing</b></summary>
302+
<summary><b>6. Retry mechanism</b></summary>
303+
304+
The HTTP Clients offers a `retry` method to retry the request automatically if the request attempt fails.
305+
306+
The `retry` method specifies the maximum number of attempts, the interval in milliseconds between attempts, and an optional callback function to determine whether a retry should be attempted based on the response, the request and the error instance.
307+
308+
```typescript
309+
import { HttpClient } from '@wolfpackthatcodes/http-client';
310+
311+
const response = new HttpClient()
312+
.retry(3, 1000, (response, request, error) => {
313+
return response !== undefined && response.status !== 404;
314+
})
315+
.get('https://api.example.local/test/');
316+
```
317+
318+
</details>
319+
320+
<details>
321+
<summary><b>7. Testing</b></summary>
303322

304323
The HTTP Client offers a `fake` method that allows you to instruct the HTTP Client to return mocked responses when requests are made. The `fake` method will prevent the HTTP Client to make a HTTP request.
305324

code/src/http/httpClient.ts

Lines changed: 187 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import MockedResponses from './responses/mockedResponses';
22
import PendingRequestBody from './pendingRequest/pendingRequestBody';
3-
import PendingRequestUrl from './pendingRequest/pendingRequestUrl';
43
import { Options } from './types/options';
54
import { HttpMethods } from './types/httpMethods';
65
import { RequestAuthorization } from './types/requestAuthorization';
@@ -9,6 +8,13 @@ import { FetchOptions } from './types/fetchOptions';
98
import { RequestOptions } from './types/requestOptions';
109

1110
export default class HttpClient {
11+
/**
12+
* The base URL for the request.
13+
*
14+
* @var {string}
15+
*/
16+
private baseUrl: string = '';
17+
1218
/**
1319
* The mocked responses instance.
1420
*
@@ -23,6 +29,13 @@ export default class HttpClient {
2329
*/
2430
private options: Options = {};
2531

32+
/**
33+
* Number of attempts for the request.
34+
*
35+
* @var {number}
36+
*/
37+
private requestAttempts: number = 0;
38+
2639
/**
2740
* The request body instance.
2841
*
@@ -31,11 +44,39 @@ export default class HttpClient {
3144
private requestBody: PendingRequestBody;
3245

3346
/**
34-
* The request Url instance.
47+
* The number of times to try the request.
48+
*
49+
* @var {number}
50+
*/
51+
private retries: number = 0;
52+
53+
/**
54+
* The number of milliseconds to wait between retries.
55+
*
56+
* @var {number}
57+
*/
58+
private retryDelay: number = 0;
59+
60+
/**
61+
* The callback that will determine if the request should be retried.
62+
*
63+
* @var {function}
64+
*/
65+
private retryCallback?: (response: Response | undefined, request: this, error?: unknown) => boolean | null;
66+
67+
/**
68+
* The URL for the request.
69+
*
70+
* @returns {string}
71+
*/
72+
private url: string = '';
73+
74+
/**
75+
* The query parameters for the request URL.
3576
*
36-
* @var {PendingRequestUrl}
77+
* @returns {Record<string, string>|undefined}
3778
*/
38-
private requestUrl: PendingRequestUrl;
79+
private urlQueryParameters?: Record<string, string>;
3980

4081
/**
4182
* Create a new Http Client instance.
@@ -46,7 +87,8 @@ export default class HttpClient {
4687
constructor(baseUrl?: string, options?: object) {
4788
this.mockedResponses = new MockedResponses();
4889
this.requestBody = new PendingRequestBody();
49-
this.requestUrl = new PendingRequestUrl(baseUrl);
90+
91+
this.setBaseUrl(baseUrl);
5092
this.setOptions(options);
5193
}
5294

@@ -106,6 +148,21 @@ export default class HttpClient {
106148
return this;
107149
}
108150

151+
/**
152+
* Try the request to the given URL.
153+
*
154+
* @param {HttpMethods} method
155+
*
156+
* @returns {Promise<Response>}
157+
*/
158+
private async attemptRequest(method: HttpMethods): Promise<Response> {
159+
const url: URL = this.buildUrl();
160+
161+
return this.mockedResponses.isMocked()
162+
? this.mockedResponses.getMockedResponse(url.toString())
163+
: await fetch(url.toString(), this.buildRequestOptions(method));
164+
}
165+
109166
/**
110167
* Construct the Fetch API Options for the request.
111168
*
@@ -131,6 +188,25 @@ export default class HttpClient {
131188
return options;
132189
}
133190

191+
/**
192+
* Construct the URL for the pending request.
193+
*
194+
* @returns {URL}
195+
*/
196+
private buildUrl(): URL {
197+
let url = this.url.replace(/^\/|\/$/g, '') + '/';
198+
199+
if (!(url.startsWith('http://') || url.startsWith('https://'))) {
200+
url = this.baseUrl.replace(/\/$/, '') + '/' + url;
201+
}
202+
203+
if (this.urlQueryParameters !== undefined) {
204+
url += '?' + new URLSearchParams(this.urlQueryParameters);
205+
}
206+
207+
return new URL(url);
208+
}
209+
134210
/**
135211
* Specify the request's content type.
136212
*
@@ -142,6 +218,19 @@ export default class HttpClient {
142218
return this.withHeader('Content-Type', contentType);
143219
}
144220

221+
/**
222+
* Delays the execution of the request by the specified number of milliseconds.
223+
*
224+
* @param {number} milliseconds
225+
*
226+
* @returns {Promise<void>}
227+
*/
228+
private delayRequest(milliseconds: number): Promise<void> {
229+
return new Promise((resolve) => {
230+
setTimeout(resolve, milliseconds);
231+
});
232+
}
233+
145234
/**
146235
* Process a DELETE request to the given URL.
147236
*
@@ -150,7 +239,9 @@ export default class HttpClient {
150239
* @returns {Promise<Response>}
151240
*/
152241
public delete(url: string): Promise<Response> {
153-
return this.sendRequest('DELETE', url);
242+
this.withUrl(url);
243+
244+
return this.sendRequest('DELETE');
154245
}
155246

156247
/**
@@ -175,9 +266,11 @@ export default class HttpClient {
175266
* @returns {Promise<Response>}
176267
*/
177268
public get(url: string, query?: object): Promise<Response> {
269+
this.withUrl(url);
270+
178271
if (query) this.withQueryParameters(query);
179272

180-
return this.sendRequest('GET', url);
273+
return this.sendRequest('GET');
181274
}
182275

183276
/**
@@ -198,9 +291,11 @@ export default class HttpClient {
198291
* @returns {Promise<Response>}
199292
*/
200293
public head(url: string, query?: object): Promise<Response> {
294+
this.withUrl(url);
295+
201296
if (query) this.withQueryParameters(query);
202297

203-
return this.sendRequest('HEAD', url);
298+
return this.sendRequest('HEAD');
204299
}
205300

206301
/**
@@ -212,9 +307,10 @@ export default class HttpClient {
212307
* @returns {Promise<Response>}
213308
*/
214309
public patch(url: string, data: object | string): Promise<Response> {
310+
this.withUrl(url);
215311
this.withBody(data);
216312

217-
return this.sendRequest('PATCH', url);
313+
return this.sendRequest('PATCH');
218314
}
219315

220316
/**
@@ -226,9 +322,10 @@ export default class HttpClient {
226322
* @returns {Promise<Response>}
227323
*/
228324
public post(url: string, data: object | string): Promise<Response> {
325+
this.withUrl(url);
229326
this.withBody(data);
230327

231-
return this.sendRequest('POST', url);
328+
return this.sendRequest('POST');
232329
}
233330

234331
/**
@@ -240,9 +337,10 @@ export default class HttpClient {
240337
* @returns {Promise<Response>}
241338
*/
242339
public put(url: string, data: object | string): Promise<Response> {
340+
this.withUrl(url);
243341
this.withBody(data);
244342

245-
return this.sendRequest('PUT', url);
343+
return this.sendRequest('PUT');
246344
}
247345

248346
/**
@@ -276,24 +374,81 @@ export default class HttpClient {
276374
return this;
277375
}
278376

377+
/**
378+
* Specify the number of times the request should be attempted.
379+
*
380+
* @param {number} maxAttempts
381+
* @param {number} intervalMilliseconds
382+
* @param {function} callback
383+
*
384+
* @returns {this}
385+
*/
386+
retry(
387+
maxAttempts: number,
388+
intervalMilliseconds: number,
389+
callback?: (response: Response | undefined, request: this, error?: unknown) => boolean | null,
390+
): this {
391+
this.retries = maxAttempts;
392+
this.retryDelay = intervalMilliseconds;
393+
this.retryCallback = callback;
394+
395+
return this;
396+
}
397+
279398
/**
280399
* Send the request to the given URL.
281400
*
282401
* @param {HttpMethods} method
283-
* @param {string} endpoint
284402
*
285403
* @returns {Promise<Response>}
286404
*/
287-
private async sendRequest(method: HttpMethods, endpoint: string): Promise<Response> {
288-
const url = this.requestUrl.buildUrl(endpoint);
405+
private async sendRequest(method: HttpMethods): Promise<Response> {
406+
this.requestAttempts++;
289407

290-
return this.mockedResponses.isMocked()
291-
? this.mockedResponses.getMockedResponse(url.toString())
292-
: await fetch(url.toString(), this.buildRequestOptions(method));
408+
try {
409+
const response: Response = await this.attemptRequest(method);
410+
const successfulResponse: boolean = response !== undefined && response.ok;
411+
const shouldRetry = this.retryCallback ? this.retryCallback(response, this) : true;
412+
413+
if (!successfulResponse && shouldRetry === true && this.requestAttempts <= this.retries) {
414+
await this.delayRequest(this.retryDelay);
415+
416+
if (this.retryCallback) {
417+
this.retryCallback(response, this);
418+
}
419+
420+
return this.sendRequest(method);
421+
} else {
422+
return response;
423+
}
424+
} catch (error) {
425+
if (this.requestAttempts <= this.retries) {
426+
await this.delayRequest(this.retryDelay);
427+
428+
if (this.retryCallback) {
429+
this.retryCallback(undefined, this, error);
430+
}
431+
432+
return this.sendRequest(method);
433+
} else {
434+
throw error;
435+
}
436+
}
293437
}
294438

295439
/**
296-
* Set the default options for the Http Client.
440+
* Set the base URL for the request.
441+
*
442+
* @param {string} baseUrl
443+
*
444+
* @returns {void}
445+
*/
446+
private setBaseUrl(baseUrl: string = ''): void {
447+
this.baseUrl = baseUrl;
448+
}
449+
450+
/**
451+
* Set the default options for the request.
297452
*
298453
* @param {object|undefined} options
299454
*/
@@ -417,7 +572,7 @@ export default class HttpClient {
417572
* @returns {this}
418573
*/
419574
public withQueryParameters(query: object): this {
420-
this.requestUrl.withQueryParameters(query);
575+
this.urlQueryParameters = { ...this.urlQueryParameters, ...query };
421576

422577
return this;
423578
}
@@ -433,4 +588,17 @@ export default class HttpClient {
433588
public withToken(token: string, type: RequestAuthorization = 'Bearer'): this {
434589
return this.withHeader('Authorization', `${type} ${token.trim()}`);
435590
}
591+
592+
/**
593+
* Specify the URL for the request.
594+
*
595+
* @param {string} url
596+
*
597+
* @returns {this}
598+
*/
599+
public withUrl(url: string): this {
600+
this.url = url;
601+
602+
return this;
603+
}
436604
}

0 commit comments

Comments
 (0)