You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular拦截器刷新Token时SwitchMap未触发问题求助

问题:HTTP拦截器Token刷新流程失效,SwitchMap未触发

问题详情

在基于Angular的Ionic应用中实现了HTTP拦截器的Token刷新逻辑,但功能异常:switchMap完全未触发,控制台无法看到switchMap fired!日志,后续的重试请求也未发送。

相关代码实现

refreshTokens函数

async refreshTokens() {
  return new Promise((resolve, reject) => {
    this.dataService
      .post(this.apiUriService.refreshTokenUri(), {
        refresh_token: this.authData.refreshToken,
      })
      .pipe(take(1))
      .subscribe({
        next: (res: any) => {
          if (!res) {
            return reject(null);
          }
          const resData = res.data ? res.data : null;
          this.utils.setItemOnLocalStorage(
            Constants.authDataStorageItemName,
            resData
          );
          resolve(resData);
        },
        error: (error) => {
          return reject(error);
        },
      });
  });
}

handle401Error函数

async handle401Error(req: HttpRequest<any>, next: HttpHandler, error: any) {
  return from(this.refreshTokens()).pipe(
    switchMap((res: any) => {
      console.log('switchMap fired!');
      const request = this.addToken(req, res.access_token);
      return next.handle(request);
    }),
    catchError((error) => {
      this.logoutUser();
      return throwError(error);
    })
  );
}

完整拦截器类代码

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import {
  HttpRequest,
  HttpResponse,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse,
} from '@angular/common/http';
import {
  Observable,
  throwError,
  BehaviorSubject,
  tap,
  take,
  from,
  switchMap,
  catchError,
} from 'rxjs';
import { AuthService } from './auth/auth.service';
import { ApiUriService } from './api-uri.service';
import { DataService } from './data.service';
import { Constants } from '../constants/constants';
import { CommonUtilsService } from './common-utils.service';
import { UserService } from './user/user.service';

@Injectable()
export class CommonHttpInterceptor implements HttpInterceptor {
  tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>('');

  get authData() {
    return this.authService.readUserInfoFromLocalStorage();
  }

  constructor(
    private authService: AuthService,
    private router: Router,
    private apiUriService: ApiUriService,
    private dataService: DataService,
    private userService: UserService,
    private utils: CommonUtilsService
  ) {}

  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    const userInfoData = this.authService.authUserData;
    const headers: any = {};

    if (userInfoData && userInfoData.accessToken) {
      headers['Authorization'] = `Bearer ${userInfoData.accessToken}`;
    }

    const clonedRequest = request.clone({ setHeaders: headers });

    return next.handle(clonedRequest).pipe(
      tap(
        (event: HttpEvent<any>) => {
          if (event instanceof HttpResponse) {
            // case when we need to transform the response
          }
        },
        (error: HttpErrorResponse) => {
          // handle  refresh token in case of the access token is expired
          if (error.status === 401) {
            return this.handle401Error(request, next);
          }
          return throwError(error);
        }
      )
    );
  }

  logoutUser() {
    this.authService.doLogout();
    this.router.navigate(['auth/login']);
  }

  handle401Error(req: HttpRequest<any>, next: HttpHandler) {
    return from(this.refreshTokens()).pipe(
      switchMap((res: any) => {
        console.log('switchMap fired!');
        const request = this.addToken(req, res.access_token);
        return next.handle(request);
      }),
      catchError((error) => {
        this.logoutUser();
        return throwError(error);
      })
    );
  }

  // refresh access token OR logout user if request failed
  refreshTokens() {
    return new Promise((resolve, reject) => {
      this.dataService
        .post(this.apiUriService.refreshTokenUri(), {
          refresh_token: this.authData.refreshToken,
        })
        .pipe(take(1))
        .subscribe({
          next: (res: any) => {
            if (!res) {
              return reject(null);
            }
            const resData = res.data ? res.data : null;
            this.utils.setItemOnLocalStorage(
              Constants.authDataStorageItemName,
              resData
            );
            resolve(resData);
          },
          error: (error) => {
            return reject(error);
          },
        });
    });
  }

  //add token to authorization header
  private addToken(request: HttpRequest<any>, token: any) {
    const newToken = this.authService.shiftoCookieValue
      ? this.authService.shiftoCookieValue
      : token;
    if (newToken) {
      let clone: HttpRequest<any>;
      clone = request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`,
        },
      });
      return clone;
    }
    return request;
  }
}

问题原因及修复方案

核心问题1:tap操作符无法正确替换错误流

intercept方法中使用tap处理错误,但tap仅用于执行副作用,不会返回新的Observable替换原流。当捕获到401错误时,返回的handle401ErrorObservable根本没有被订阅执行,这是switchMap未触发的根本原因。

修复:用catchError替换tap处理401错误,确保正确返回刷新Token并重试请求的流。

核心问题2:Observable转Promise冗余且易出问题

refreshTokens将Observable封装成Promise完全没必要,直接使用RxJS操作符可避免异步转换的潜在问题,代码更简洁。

修复后的关键代码

修正后的intercept方法

intercept(
  request: HttpRequest<unknown>,
  next: HttpHandler
): Observable<HttpEvent<unknown>> {
  const userInfoData = this.authService.authUserData;
  const headers: any = {};

  if (userInfoData && userInfoData.accessToken) {
    headers['Authorization'] = `Bearer ${userInfoData.accessToken}`;
  }

  const clonedRequest = request.clone({ setHeaders: headers });

  return next.handle(clonedRequest).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        return this.handle401Error(request, next);
      }
      return throwError(() => error);
    })
  );
}

修正后的refreshTokens方法(去掉Promise封装)

refreshTokens(): Observable<any> {
  return this.dataService
    .post(this.apiUriService.refreshTokenUri(), {
      refresh_token: this.authData.refreshToken,
    })
    .pipe(
      take(1),
      tap((res: any) => {
        if (!res) {
          throw new Error('Refresh token response is empty');
        }
        const resData = res.data ? res.data : null;
        this.utils.setItemOnLocalStorage(
          Constants.authDataStorageItemName,
          resData
        );
      })
    );
}

修正后的handle401Error方法(无需from转换)

handle401Error(req: HttpRequest<any>, next: HttpHandler) {
  return this.refreshTokens().pipe(
    switchMap((res: any) => {
      console.log('switchMap fired!');
      const request = this.addToken(req, res.access_token);
      return next.handle(request);
    }),
    catchError((error) => {
      this.logoutUser();
      return throwError(() => error);
    })
  );
}

额外优化点

  • addToken方法中newToken变量定义后未使用,直接用了传入的token,可修正该逻辑避免混淆。
  • 处理并发请求:若多个请求同时触发401,可利用tokenSubject控制并发,防止重复调用刷新Token接口。

内容的提问来源于stack exchange,提问作者nabilinfo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 04:45:59