import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse, HttpStatusCode, } from '@angular/common/http'; import { Inject, Injectable, Optional } from '@angular/core'; import { finalize, MonoTypeOperatorFunction, Observable, retry, throwError, timer } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; import { LoadingService } from '../loading'; import { getConfig } from '../services'; import { KeycloakStatusService } from '../tenants'; import { ErrorService } from './error.service'; import { SERVER_ERROR_SKIP_PATHS } from './tokens'; function updateSeconds(seconds: number) { if (seconds === 0 || seconds === 1) { return seconds + 1; } return seconds * seconds; } function isNotServerError(error: HttpErrorResponse) { return error.status < HttpStatusCode.InternalServerError && error.status !== 0; } function isPathSkipped(path: string, skippedPaths: string[]) { const sanitized = path.replace('https://', '').replace('http://', ''); const pathWithoutHost = sanitized.split(/\/(.*)/s)[1]; return skippedPaths.some(skippedPath => pathWithoutHost === skippedPath); } function backoffOnServerError(maxRetries = 3, skippedPaths: string[] = []): MonoTypeOperatorFunction> { let seconds = 0; function delay(error: HttpErrorResponse) { seconds = updateSeconds(seconds); if (isNotServerError(error) || (error.url && isPathSkipped(error.url, skippedPaths))) { return throwError(() => error); } console.error('An error occurred: ', error); console.error(`Retrying in ${seconds} seconds...`); return timer(seconds * 1000); } return retry({ count: maxRetries, delay }); } @Injectable() export class ServerErrorInterceptor implements HttpInterceptor { private readonly _urlsWithError = new Set(); readonly #config = getConfig(); constructor( private readonly _errorService: ErrorService, private readonly _loadingService: LoadingService, private readonly _keycloakStatusService: KeycloakStatusService, @Optional() @Inject(SERVER_ERROR_SKIP_PATHS) private readonly _skippedPaths: string[], ) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { return next.handle(req).pipe( tap(event => { if (event instanceof HttpResponse && this._urlsWithError.has(req.url)) { this._errorService.setOnline(); this._urlsWithError.delete(req.url); } }), catchError((err: unknown) => { const error = err as HttpErrorResponse; // token expired if (error.status === HttpStatusCode.Unauthorized) { this._keycloakStatusService.createLoginUrlAndExecute(); } // server error if (error.status >= HttpStatusCode.InternalServerError && error.url && !isPathSkipped(error.url, this._skippedPaths)) { this._errorService.set(error); this._urlsWithError.add(req.url); } return throwError(() => error).pipe( finalize(() => { this._loadingService.stop(); }), ); }), backoffOnServerError(this.#config.MAX_RETRIES_ON_SERVER_ERROR, this._skippedPaths), ); } }