Angular Interceptor - перехватчик ошибок http
Чистое и лаконичное управление ошибками http.
Время от времени в любом приложении могут возникать ошибки, необходимо внедрить подходящую систему обработки ошибок. В этой статье мы расскажем про различные типы http ошибок и то, как мы можем их решить.
Типы ошибок http
Есть много причин по которым HTTP-запросы не выполняются. Плохое сетевое соединение, неправильный URL-адрес или запрошенный сервер в настоящее время недоступен, и это лишь некоторые из них. В целом ошибки можно разделить на два типа: ошибки на стороне сервера и ошибки на стороне клиента.
С обоими типами нужно обращаться по-разному, поскольку мы, клиенты, несем ответственность за последние, мы можем обращаться с ними иначе, чем с первыми. В случае ошибок на стороне сервера мы можем передать только соответствующее сообщение об ошибке, полученное от сервера или попытаться повторить попытку, поскольку сервер мог быть временно недоступен.
Однако на стороне клиента мы могли использовать неверный URL-адрес или отсутствующий параметр и нам нужно это исправить.
В обоих случаях важно предоставлять пользователю сообщения об ошибках, для которых также подходит глобальная обработка ошибок.
HTTP Interceptor
С помощью так называемого HttpInterceptor
, Angular предоставляет нам способ преобразовывать http-запросы и ответы.
Через интерфейс мы можем реализовать intercept
метод, который вызывается автоматически при каждом HTTP-запросе, сделанном через наше приложение. Это позволяет нам реализовать централизованную обработку ошибок с помощью перехватчика.
Перехватчик имеет следующие параметры:
- req — объект исходящего запроса для обработки.
- next — следующий перехватчик в цепочке или серверная часть, если в цепочке не осталось перехватчиков.
- Returns: наблюдаемая часть потока событий.
Давайте рассмотрим пример.
Обработчик HTTP-ошибок
Мы можем создать новый перехватчик с помощью Angular CLI
со следующей командой:
ng generate interceptor http-error
Обычно мы держим свои перехватчики в CoreModule, который содержит глобальные функции и загружается сразу после запуска приложения. Перехватчик должен быть зарегистрирован в модуле. Мы можем сделать это, добавив его в массив провайдеров:
@NgModule({
imports: [CommonModule, HttpClientModule],
declarations: [,
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: HttpErrorInterceptor,
multi: true,
},
],
})
export class CoreModule {}
Свойство multi
должно быть установлено на true
, т.к. может быть несколько перехватчиков, использующих HTTP_INTERCEPTORS
токен внедрения.
Затем мы реализуем intercept
метод, в котором мы добавляем catchError
оператор RxJS к запросу, который будет использоваться для нашей логики обработки ошибок.
@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError((err) => {
if (err instanceof ErrorEvent) {
console.log('this is an error in the code');
} else {
console.log('this is an error return by the server');
}
return throwError(err);
}),
);
}
}
Внутри catchError
мы проверяем, является ли ошибка экземпляром ErrorEvent
, и в этом случае тип неизвестен, и вероятно, что-то не так с нашим кодом. В противном случае мы можем проверить ответ сервера и соответствующим образом обработать его.
Как уже упоминалось, важно отображать осмысленное сообщение для пользователя независимо от типа ошибки. Для этой цели мы можем определить интерфейс, чтобы он имел общую структуру для всех сообщений об ошибках, например, как показано ниже:
export enum ErrorSeverity {
INFO = 'INFO',
WARNING = 'WARNING',
ERROR = 'ERROR',
FATAL = 'FATAL',
}
export interface BackendError {
title?: string;
message: string;
severity: ErrorSeverity;
code: string;
}
У нас есть необязательный заголовок, который мы можем использовать, например, для отображения всплывающего сообщения. Затем само сообщение об ошибке, различные уровни важности и код ответа http. Теперь добавим интерфейс к нашему перехватчику:
@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError((err) => {
let error: BackendError;
if (err instanceof ErrorEvent) {
// this is client side error
error = this.handleUnknownError();
} else {
// this is server side error
error = this.handleBackendError(error, err);
}
return throwError(() => error);
})
);
}
private handleUnknownError(): BackendError {
// this is not from backend. Format our own message.
return {
message: 'Unknown error!',
severity: ErrorSeverity.FATAL,
code: 'UNKNOWN_ERROR',
};
}
private handleBackendError(error: BackendError, err): BackendError {
// Backend returned error, format it here
return {
title: err.error?.title || 'Default title',
message: err.error && err.error.message ? err.error.message : err.error ? err.error : err.message,
severity: ErrorSeverity.ERROR,
code: err.error?.identifierCode ? err.error.identifierCode : 'BACKEND_ERROR',
};
}
}
Мы преобразуем ошибку для обоих типов, а затем повторно выдаем ошибку, чтобы она могла быть обработана соответствующей службой, из которой изначально был инициирован запрос.
Затем мы можем интегрировать логику повторных попыток для определенных кодов ответов, упомянутых выше. Повтор для http-запросов можно реализовать достаточно просто с помощью RxJS. Для этого есть два оператора: retry
и retryWhen
.
Любой из них можно использовать для повторения наблюдаемой последовательности заданное количество раз. В данном случае retryWhen
больше подходит оператор, так как мы можем работать с произвольными условиями, а не только с количеством повторных попыток.
Для нашей стратегии повторных попыток мы можем создать собственный оператор:
export const genericRetryStrategy =
({
maxRetryAttempts = 3,
scalingDuration = 1000,
excludedStatusCodes = [],
}: {
maxRetryAttempts?: number;
scalingDuration?: number;
excludedStatusCodes?: [];
} = {}) =>
(attempts: Observable<any>) => {
return attempts.pipe(
mergeMap((error, i) => {
const retryAttempt = i + 1;
if (retryAttempt > maxRetryAttempts || excludedStatusCodes.find((e) => e === error.status)) {
return throwError(error);
}
return timer(retryAttempt * scalingDuration);
}),
);
};
Настраиваемая повторная попытка с увеличенной продолжительностью.
Мы можем настроить наш оператор, используя три параметра. Во-первых с количеством попыток, во-вторых мы можем использовать scalingDuration
для создания задержки между попытками и в-третьих любые http-коды можно исключить, например 404, т.к. повторная попытка, например, для «404 — не найдено» не имеет смысла.
Новый оператор теперь можно просто вставить в pipe нашего перехватчика вместе с retryWhen:
@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
retryWhen(genericRetryStrategy({ excludedStatusCodes: [404, ...] })),
catchError((err) => {
...
);
}
}