Last active
July 24, 2024 17:53
-
-
Save imran3/b5dff3e89b7689a7f5ac534d7bd4cacf to your computer and use it in GitHub Desktop.
Retry http errors mechanism in Angular
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HttpClient, HttpErrorResponse, HttpResponse, HTTP_INTERCEPTORS } from '@angular/common/http'; | |
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; | |
import { fakeAsync, TestBed, tick } from '@angular/core/testing'; | |
import { AppConfig } from '../../services/app-config/app-config.interface'; | |
import { AppConfigService } from '../../services/app-config/app-config.service'; | |
import { MOCK_APP_CONFIG } from '../../services/app-config/mock/get-config.mock'; | |
import { RetryHttpErrorsInterceptor } from './retry-http-errors.interceptor'; | |
describe('RetryHttpErrorsInterceptor', () => { | |
let interceptor: RetryHttpErrorsInterceptor; | |
let appConfigService: AppConfigService; | |
let httpClient: HttpClient; | |
let httpTestingController; | |
beforeEach(() => { | |
TestBed.configureTestingModule({ | |
imports: [HttpClientTestingModule], | |
providers: [ | |
RetryHttpErrorsInterceptor, | |
{ | |
provide: HTTP_INTERCEPTORS, | |
useClass: RetryHttpErrorsInterceptor, | |
multi: true, | |
}, | |
{ | |
provide: AppConfigService, | |
useValue: { | |
getConfig: jest.fn(), | |
}, | |
}, | |
], | |
}); | |
appConfigService = TestBed.inject(AppConfigService); | |
httpClient = TestBed.inject(HttpClient); | |
httpTestingController = TestBed.inject(HttpTestingController); | |
}); | |
it('should be created', () => { | |
interceptor = TestBed.inject(RetryHttpErrorsInterceptor); | |
expect(interceptor).toBeTruthy(); | |
}); | |
describe('Using AppConfig', () => { | |
let mockAppConfig: AppConfig; | |
beforeEach(() => { | |
mockAppConfig = { | |
...MOCK_APP_CONFIG, | |
maximumRetries: 5, | |
}; | |
appConfigService.getConfig = jasmine.createSpy().and.returnValue(mockAppConfig); | |
}); | |
it('should retry configured maximum retries times', fakeAsync(() => { | |
const requesturl = '/foo/bar'; | |
const errorMsg = 'Something went wrong'; | |
const errorResponse: HttpErrorResponse = new HttpErrorResponse({ | |
status: 502, | |
statusText: 'Some status text for 502', | |
}); | |
httpClient.get(requesturl).subscribe( | |
_ => fail('should have failed with status code 502'), | |
(error: HttpErrorResponse) => { | |
expect(error.error).toEqual(errorMsg); | |
expect(error.status).toEqual(errorResponse.status); | |
expect(error.statusText).toEqual(errorResponse.statusText); | |
}, | |
); | |
// simulate failed responses | |
for (let i = 0; i < mockAppConfig.maximumRetries + 1; i++) { | |
const req = httpTestingController.expectOne(requesturl); | |
req.flush(errorMsg, errorResponse); | |
tick((i + 1) * mockAppConfig.retryDelay); | |
} | |
})); | |
it('should retry 2 time and get valid response on last retry', fakeAsync(() => { | |
const requesturl = '/foo/bar'; | |
const errorMsg = 'Something went wrong'; | |
const errorResponse: HttpErrorResponse = new HttpErrorResponse({ | |
status: 502, | |
statusText: 'Some status text for 502', | |
}); | |
const successResponse = { foo: 'bar' }; | |
httpClient.get(requesturl).subscribe( | |
(response: HttpResponse<any>) => { | |
expect(response).toEqual(successResponse); | |
}, | |
_ => fail('Should have receive success response on last retry'), | |
); | |
// simulate failed responses | |
for (let i = 0; i < mockAppConfig.maximumRetries - 2; i++) { | |
const req = httpTestingController.expectOne(requesturl); | |
req.flush(errorMsg, errorResponse); | |
tick((i + 1) * mockAppConfig.retryDelay); | |
} | |
// simulate success response | |
const req = httpTestingController.expectOne(requesturl); | |
req.flush(successResponse); | |
tick(mockAppConfig.retryDelay); | |
})); | |
it('should not retry success response', fakeAsync(() => { | |
const requesturl = '/foo/bar'; | |
const successResponse = { foo: 'bar' }; | |
httpClient.get(requesturl).subscribe( | |
(response: HttpResponse<any>) => { | |
expect(response).toEqual(successResponse); | |
}, | |
_ => fail('Should have received success response'), | |
); | |
// simulate success response | |
const req = httpTestingController.expectOne(requesturl); | |
req.flush(successResponse); | |
tick(mockAppConfig.retryDelay); | |
})); | |
it('should no retry client error (status code < 500)', fakeAsync(() => { | |
const requesturl = '/foo/bar'; | |
const errorMsg = 'Something went wrong'; | |
const errorResponse: HttpErrorResponse = new HttpErrorResponse({ | |
status: 404, | |
statusText: 'Some status text for 404', | |
}); | |
httpClient.get(requesturl).subscribe( | |
_ => fail('should have failed with status code 404'), | |
(error: HttpErrorResponse) => { | |
expect(error.error).toEqual(errorMsg); | |
expect(error.status).toEqual(errorResponse.status); | |
expect(error.statusText).toEqual(errorResponse.statusText); | |
}, | |
); | |
const req = httpTestingController.expectOne(requesturl); | |
req.flush(errorMsg, errorResponse); | |
tick(mockAppConfig.retryDelay); | |
})); | |
afterEach(() => { | |
// verify that there are no outstanding request after each test run | |
httpTestingController.verify(); | |
}); | |
}); | |
describe('Using default config', () => { | |
it('should retry default maximum retries times', fakeAsync(() => { | |
appConfigService.getConfig = jasmine.createSpy().and.returnValue(null); | |
const requesturl = '/foo/bar'; | |
const errorMsg = 'Something went wrong'; | |
const errorResponse: HttpErrorResponse = new HttpErrorResponse({ | |
status: 501, | |
statusText: 'Some status text for 501', | |
}); | |
httpClient.get(requesturl).subscribe( | |
_ => fail('should have failed with status code 501'), | |
(error: HttpErrorResponse) => { | |
expect(error.error).toEqual(errorMsg); | |
expect(error.status).toEqual(errorResponse.status); | |
expect(error.statusText).toEqual(errorResponse.statusText); | |
}, | |
); | |
for (let i = 0; i < 3; i++) { | |
const req = httpTestingController.expectOne(requesturl); | |
req.flush(errorMsg, errorResponse); | |
tick((i + 1) * 200); | |
} | |
})); | |
afterEach(() => { | |
httpTestingController.verify(); | |
}); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; | |
import { Injectable } from '@angular/core'; | |
import { retryWhen, concatMap, delay } from 'rxjs/operators'; | |
import { iif, throwError, of, Observable } from 'rxjs'; | |
import { AppConfigService } from '../../services/app-config/app-config.service'; // dynamic config service | |
export interface RetryRequestOptions { | |
maximumRetries: number; | |
retryDelay: number; | |
} | |
@Injectable() | |
export class RetryHttpErrorsInterceptor implements HttpInterceptor { | |
// default retry options | |
private retryRequestOptions: RetryRequestOptions = { | |
maximumRetries: 2, | |
retryDelay: 200, //milliseconds | |
}; | |
private retryOptionConfigured = false; | |
constructor(private appConfigService: AppConfigService) {} | |
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |
// first time it run: need to replace default retry config options | |
if (!this.retryOptionConfigured) { | |
let appConfig = this.appConfigService.getConfig(); | |
if (appConfig) { | |
this.retryRequestOptions = { | |
maximumRetries: appConfig.maximumRetries, | |
retryDelay: appConfig.retryDelay, | |
}; | |
this.retryOptionConfigured = true; | |
} | |
} | |
return next.handle(request).pipe(this.retryPipe()); | |
} | |
retryPipe<T>() { | |
return retryWhen<T>((errors: Observable<HttpErrorResponse>) => | |
errors.pipe( | |
// Use concat map to keep the errors in order and make sure they | |
// aren't executed in parallel | |
concatMap((error: HttpErrorResponse, retryId: number) => | |
// Executes a conditional Observable depending on the result | |
// of the first argument | |
iif( | |
() => retryId >= this.retryRequestOptions.maximumRetries || error.status <= 500, | |
// If the condition is true we throw the error (the last error) | |
this.returnError(error), | |
// Otherwise we pipe this back into our stream and delay the retry | |
this.retryError(error, retryId, this.retryRequestOptions.retryDelay), | |
), | |
), | |
), | |
); | |
} | |
returnError(error: HttpErrorResponse) { | |
return throwError(error); | |
} | |
retryError(error: HttpErrorResponse, retryId: number, retryDelay: number) { | |
// retry request with increasing delay between each attemp | |
return of(error).pipe(delay(retryDelay * retryId)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment