Angular中遇401错误时如何排队HTTP请求并自动重试?
Angular JWT令牌刷新:并行请求排队重试解决方案
你的核心问题是多个并行请求(如forkJoin发起的请求)遇到401时,无法统一等待令牌刷新完成后重试。以下是针对你现有拦截器的优化方案,解决并行请求的排队问题:
关键问题分析
- 初始请求未自动带令牌:现有拦截器仅在重试时添加令牌,导致所有请求默认无令牌发送,触发不必要的401错误。
- 并行请求重复触发刷新:原逻辑虽用
isRefreshingToken标记,但未统一管理刷新请求,可能导致潜在的重复刷新(或等待逻辑失效)。 forkJoin错误传播:若初始请求的401未被拦截器正确转换为重试Observable,forkJoin会因单个错误终止所有请求。
修改后的完整拦截器代码
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http'; import { Observable, BehaviorSubject, throwError, filter, take, switchMap, map } from 'rxjs'; import { Router } from '@angular/router'; import { LoggerService } from './logger.service'; // 替换为你的实际服务路径 import { AuthenticationService } from './authentication.service'; // 替换为你的实际服务路径 import { StatusCodes } from 'http-status-codes'; import { appSettings } from '../app-settings'; // 替换为你的实际配置 import { environment } from '../environments/environment'; // 替换为你的实际环境配置 import { IResponse, IAuthResult } from './models'; // 替换为你的实际模型 @Injectable() export class HttpErrorInterceptor implements HttpInterceptor { constructor( private _router: Router, private _logger: LoggerService, private _authService: AuthenticationService ) {} private isRefreshingToken = false; private tokenSubject: BehaviorSubject<string | null> = new BehaviorSubject<string | null>(null); private refreshTokenObservable: Observable<string | null> | null = null; public intercept( request: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<IResponse>> { // 1. 请求发送前自动添加当前有效的access token const currentToken = this._authService.getAccessToken(); const authRequest = currentToken ? this.addTokenInHeader(request, currentToken) : request; return next.handle(authRequest).pipe( timeout(appSettings.ajaxTimeout), catchError((error) => this.errorHandler(error, request, next)) ); } private errorHandler( error: HttpErrorResponse, request: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<IResponse>> { if (error.error instanceof ErrorEvent) { if (!environment.production) { this._logger.log('客户端/网络错误: ' + error); } } else { const httpErrorCode = error.status; switch (httpErrorCode) { case StatusCodes.INTERNAL_SERVER_ERROR: this._router.navigate(['/internal-server-error']); break; case StatusCodes.UNAUTHORIZED: return this.handle401Error(request, next); default: this._logger.log('请求错误: ' + error); break; } } return throwError(() => error.error || error); } private handle401Error( request: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { // 2. 仅发起一次令牌刷新请求,所有并行请求等待同一结果 if (!this.isRefreshingToken) { this.isRefreshingToken = true; this.tokenSubject.next(null); // 存储当前刷新请求,供后续请求订阅 this.refreshTokenObservable = this._authService.regenerateTokens().pipe( map((apiResult) => { const authData = apiResult.dataset as IAuthResult; this._authService.updateRefreshedTokens(authData); const newToken = authData.tokens.access_token; this.tokenSubject.next(newToken); return newToken; }), catchError((error) => { // 刷新失败:登出并清空令牌 this._authService.logout(); this._router.navigate(['/']); this.tokenSubject.next(null); return throwError(() => error); }), finalize(() => { this.isRefreshingToken = false; this.refreshTokenObservable = null; }) ); } // 3. 所有等待的请求订阅同一刷新结果,拿到新令牌后重试 return this.refreshTokenObservable.pipe( filter((token) => token !== null), take(1), switchMap((token) => { return next.handle(this.addTokenInHeader(request, token)); }) ); } private addTokenInHeader( request: HttpRequest<any>, token: string | null ): HttpRequest<any> { return request.clone({ setHeaders: { Authorization: 'Bearer ' + token } }); } }
核心优化点说明
- 请求前置令牌添加:在
intercept方法中自动为所有请求添加当前有效的access token,减少不必要的401触发。 - 统一刷新请求管理:用
refreshTokenObservable存储当前的刷新请求,确保所有并行401请求都等待同一刷新操作完成,避免重复刷新。 - 并行请求排队重试:后续请求通过订阅同一个刷新Observable,等待新令牌生成后自动重试,确保
forkJoin能获取所有请求的重试结果。
配套要求
确保你的AuthenticationService实现以下方法:
getAccessToken(): 返回当前有效的access token(从内存/localStorage获取)regenerateTokens(): 发起刷新令牌的API请求,返回包含新令牌的ObservableupdateRefreshedTokens(authData: IAuthResult): 更新本地存储的令牌信息logout(): 执行登出逻辑,清空令牌并跳转登录页
内容的提问来源于stack exchange,提问作者RAHUL KUNDU
相关产品推荐
相关产品推荐

