در دنیای واقعی، تمام درخواستهای HTTP ارسالی به سمت سرور، با موفقیت به آن نمیرسند. ممکن است در یک لحظه سرور در دسترس نباشد. در لحظهای دیگر آنقدر بار آن بالا باشد که نتواند درخواست شما را پردازش کند و یا ممکن است درست در لحظهای که توکن دسترسی به برنامه در حال به روز رسانی است، درخواست دیگری به سمت سرور ارسال شده باشد که حتما برگشت خواهد خورد؛ چون حاوی توکن جدید صادر شده نیست. در تمام این موارد ضرورت تکرار و سعی مجدد درخواستهای شکست خورده وجود دارد. برای مدیریت این مساله در برنامههای Angular میتوان از امکانات توکار کتابخانهی RxJS به همراه آن کمک گرفت.
سعی مجدد خودکار درخواستها توسط کتابخانهی RxJS
با استفاده از عملگر retry میتوان به صورت خودکار، درخواستهای شکست خورده را به تعداد باری که مشخص میشود، تکرار کرد:
import { Observable } from "rxjs";
import { catchError, map, retry } from "rxjs/operators";
postEmployeeForm(employee: Employee): Observable<Employee> {
const headers = new HttpHeaders({ "Content-Type": "application/json" });
return this.http
.post(this.baseUrl, employee, { headers: headers })
.pipe(
map((response: any) => response["fields"] || {}),
catchError(this.handleError),
retry(3)
);
}
در اینجا اگر درخواست اول با شکست مواجه شود، اصل آنرا سه بار دیگر به سمت سرور ارسال میکند (البته در صورت بروز و دریافت خطای مجدد).
مشکل این روش در عدم وجود مکثی بین درخواستها است. در اینجا تمام درخواستهای سعی مجدد، بلافاصله به سمت سرور ارسال میشوند. همچنین نمیتوان مشخص کرد که اگر مثلا خطای timeout وجود داشت، اینکار را تکرار کن و نه برای سایر حالات.
سفارشی سازی سعی مجدد خودکار درخواستها، توسط کتابخانهی RxJS
برای اینکه بتوان کنترل بیشتری را بر روی سعیهای مجدد انجام شده داشت، میتوان از عملگر retryWhen بجای retry استفاده کرد:
import { Observable, of, throwError as observableThrowError, throwError } from "rxjs";
import { catchError, delay, map, retryWhen, take } from "rxjs/operators";
postEmployeeForm(employee: Employee): Observable<Employee> {
const headers = new HttpHeaders({ "Content-Type": "application/json" });
return this.http
.post(this.baseUrl, employee, { headers: headers })
.pipe(
map((response: any) => response["fields"] || {}),
retryWhen(errors => errors.pipe(
delay(1000),
take(3)
)),
catchError(this.handleError)
);
}
در اینجا توسط عملگر retryWhen، کار سفارشی سازی سعیهای مجدد درخواستهای شکست خورده انجام شدهاست. در این مثال پس از هر درخواست مجدد، 1000ms صبر شده و سپس درخواست دیگری درصورت وجود خطا، به سمت سرور تا حداکثر 3 بار، ارسال میشود.
در ادامه اگر بخواهیم صرفا به خطاهای خاصی واکنش نشان دهیم میتوان به صورت زیر عمل کرد:
import { Observable, of, throwError as observableThrowError, throwError } from "rxjs";
import { catchError, delay, map, mergeMap, retryWhen, take } from "rxjs/operators";
postEmployeeForm(employee: Employee): Observable<Employee> {
const headers = new HttpHeaders({ "Content-Type": "application/json" });
return this.http
.post(this.baseUrl, employee, { headers: headers })
.pipe(
map((response: any) => response["fields"] || {}),
retryWhen(errors => errors.pipe(
mergeMap((error: HttpErrorResponse, retryAttempt: number) => {
if (retryAttempt === 3 - 1) {
console.log(`HTTP call failed after 3 retries.`);
return throwError(error); // no retry
}
switch (error.status) {
case 400:
case 404:
return throwError(error); // no retry
}
return of(error); // retry
}),
delay(1000),
take(3)
)),
catchError(this.handleError)
);
}
در اینجا با اضافه شدن یک mergeMap، پیش از ارسال درخواست مجدد، به اطلاعات خطای رسیدهی از سمت سرور دسترسی خواهیم داشت. همچنین پارامتر دوم mergeMap، شماره سعی جاری را نیز بر میگرداند.
در داخل mergeMap اگر یک Observable معمولی بازگشت داده شود، به معنای صدور مجوز سعی مجدد است؛ اما اگر throwError بازگشت داده شود، دقیقا در همان لحظه کار retryWhen و سعیهای مجدد خاتمه خواهد یافت. برای مثال در اینجا پس از 2 بار سعی مجدد، اصل خطا صادر میشود که سبب خواهد شد قسمت catchError اجرا شود و یا روش صرفنظر کردن از خطاهای با شمارههای 400 یا 404 را نیز مشاهده میکنید. برای مثال اگر از سمت سرور خطای 404 و یا «یافت نشد» صادر شد، return throwError سبب خاتمهی سعیهای مجدد و خاتمهی عملیات retryWhen میشود.
سعی مجدد تمام درخواستهای شکست خوردهی کل برنامه
روش فوق را باید به ازای تک تک درخواستهای HTTP برنامه تکرار کنیم. برای مدیریت یک چنین اعمال تکراری در برنامههای Angular میتوان یک HttpInterceptor سفارشی را تدارک دید و توسط آن تمام درخواستهای HTTP سراسر برنامه را به صورت متمرکز تحت نظر قرار داد:
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, of, throwError } from "rxjs";
import { catchError, delay, mergeMap, retryWhen, take } from "rxjs/operators";
@Injectable()
export class RetryInterceptor implements HttpInterceptor {
private delayBetweenRetriesMs = 1000;
private numberOfRetries = 3;
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
retryWhen(errors => errors.pipe(
mergeMap((error: HttpErrorResponse, retryAttempt: number) => {
if (retryAttempt === this.numberOfRetries - 1) {
console.log(`HTTP call '${request.method} ${request.url}' failed after ${this.numberOfRetries} retries.`);
return throwError(error); // no retry
}
switch (error.status) {
case 400:
case 404:
return throwError(error); // no retry
}
return of(error); // retry
}),
delay(this.delayBetweenRetriesMs),
take(this.numberOfRetries)
)),
catchError((error: any, caught: Observable<HttpEvent<any>>) => {
console.error({ error, caught });
if (error.status === 401 || error.status === 403) {
// this.router.navigate(["/accessDenied"]);
}
return throwError(error);
})
);
}
}
RetryInterceptor فوق، تمام درخواستهای با شکست مواجه شده را دو بار با فاصله زمانی یک ثانیه تکرار میکند. البته در این بین همانطور که توضیح داده شد، از خطاهای 400 و 404 صرفنظر خواهد شد. همچنین در پایان کار اگر سعیهای مجدد با موفقیت به پایان نرسند، قسمت catchError، اصل خطای رخ داده را دریافت میکند که در اینجا نیز میتوان به این خطا عکس العمل نشان داد.
روش ثبت آن در قسمت providers مربوط به core.module.ts به صورت زیر است:
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: RetryInterceptor,
multi: true
},