96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
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<HttpEvent<unknown>> {
|
|
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<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
|
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),
|
|
);
|
|
}
|
|
}
|