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

Axios拦截器刷新Token前弹出401错误的问题排查

Axios拦截器Token刷新失效:401错误提前触发catch块

我尝试用Axios拦截器实现Token自动刷新,预期当Access Token过期时,调用点赞/取消点赞接口会先触发刷新Token接口,获取新Token后重发原请求,实现静默处理。但实际情况是客户端的catch块先捕获到401错误并弹出提示,导致应用异常,拦截器的刷新逻辑反而在错误提示之后才执行。


相关代码

AxiosConfig.js

import axios from "axios";
import { store } from "./redux/store";
import { login, logout } from "./redux/slices/user";

const baseURL = process.env.NEXT_PUBLIC_API_URL;

axios.defaults.baseURL = baseURL;

export const axiosInstance = axios.create({
  withCredentials: true,
  headers: {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
    accept: "application/json",
  },
});

axiosInstance.interceptors.request.use(
  (config) => {
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

axiosInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    const originalRequest = error?.config;
    if (error?.response?.status === 401 && !originalRequest.sent) {
      originalRequest.sent = true;
      const response = await axios.get("/auth/refresh", {
        withCredentials: true,
      });
      if (response.error.status === 403) {
        store.dispatch(logout());
      }
      store.dispatch(login({ user: response.data.user }));
      return axiosInstance(originalRequest);
    }
    return Promise.reject(error);
  }
);

VerifyJwt.js

export const verifyToken = (req, res, next) => {
  try {
    const accessToken = req.cookies.accessToken;
    if (!accessToken)
      return res.status(403).json({
        message: "Not authenticated, no token provided",
      });
    jwt.verify(accessToken, process.env.ACCESS_SECRET_KEY, (err, decoded) => {
      if (err)
        return res.status(401).json({
          message: "Not logged in, invalid token",
        });
      req.user = decoded.id;
      next();
    });
  } catch (e) {
    res
      .status(500)
      .json({ message: "Something went wrong while verifying jwt" });
  }
};

routes/video.js

import express from "express";
import { unAuthVerify, verifyToken } from "../middlewares/verifyJwt.js";
import {
  deleteVideo,
  dislikeVideo,
  getVideoDetails,
  getVideos,
  likeVideo,
  updateVideo,
  uploadVideo,
} from "../controllers/videos.js";

const router = express.Router();
router.get("/", getVideos);
router.post("/", verifyToken, uploadVideo);
router.get("/:id", unAuthVerify, getVideoDetails);
router.put("/:id", verifyToken, updateVideo);
router.delete("/:id", verifyToken, deleteVideo);
router.put("/like/:id", verifyToken, likeVideo);
router.put("/dislike/:id", verifyToken, dislikeVideo);

export default router;

RefreshToken.js

export const refreshToken = async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  try {
    if (refreshToken) {
      jwt.verify(
        refreshToken,
        process.env.REFRESH_SECRET_KEY,
        async (err, decoded) => {
          if (err) {
            return res.status(403).json({
              message: "Invalid token/not logged in",
            });
          }
          const accessToken = jwt.sign(
            { id: decoded.id },
            process.env.ACCESS_SECRET_KEY,
            {
              expiresIn: "1m",
            }
          );
          const foundUser = await db.query(
            "SELECT * FROM channels WHERE id = $1",
            [decoded.id]
          );
          if (foundUser.rows.length <= 0) {
            return res.status(400).json({
              message: "User/Channel not found",
            });
          }
          const { password, ...others } = foundUser.rows[0];
          if (foundUser.rows.length > 0) {
            res.cookie("accessToken", accessToken, {
              httpOnly: true,
              secure: true,
            });
            return res.status(200).json({
              user: others,
            });
          }
        }
      );
    } else {
      return res.status(403).json({
        message: "Invalid token/not logged in",
      });
    }
  } catch (e) {
    console.log(e);
    res.status(500).json({
      message: "Something went wrong.Please try again",
    });
  }
};

Video.jsx

const handleLike = async (id) => {
   if (!isLoggedIn) {
     toast.error("You need to login to like a video");
     return;
   }
   if (likedByMe) {
     toast.success("You have already liked this video");
     return;
   }

   try {
     await axiosInstance.put(`/videos/like/${id}`);
     setLikes((prev) => prev + 1);
     if (likedByMe === false) {
       setDisLikes((prev) => prev - 1);
     }
     setLikedByMe(true);
   } catch (e) {
     console.log("Something went wrong while liking video", e);
     toast.error(e.response.data.message);  // 这里先捕获到401错误弹出提示,之后拦截器才调用刷新接口
   }
 };

问题原因分析

  1. 刷新请求未使用配置好的axios实例:拦截器中用全局axios发送刷新请求,而非axiosInstance,导致请求配置不统一,可能刷新请求本身失败,无法获取新Token。
  2. 错误处理逻辑错误:拦截器中直接访问response.error.status是错误的,刷新请求成功时response没有error属性,应该用try/catch捕获刷新请求的异常。
  3. 未标记请求重试状态:原代码用sent标记,但逻辑不严谨,可能导致重复触发刷新请求。

解决方案

1. 修正Axios拦截器代码

axiosInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    const originalRequest = error?.config;
    // 只处理401且未标记为重试的请求
    if (error?.response?.status === 401 && originalRequest && !originalRequest._retry) {
      originalRequest._retry = true; // 标记为已重试,避免循环请求
      try {
        // 使用同一个axiosInstance发送刷新请求
        const refreshResponse = await axiosInstance.get("/auth/refresh");
        // 更新store中的用户信息
        store.dispatch(login({ user: refreshResponse.data.user }));
        // 重发原请求
        return axiosInstance(originalRequest);
      } catch (refreshError) {
        // 刷新Token失败,触发登出
        if (refreshError.response?.status === 403) {
          store.dispatch(logout());
          toast.error("登录已过期,请重新登录");
        }
        // 抛出刷新错误,让上层处理
        return Promise.reject(refreshError);
      }
    }
    // 非401错误或已重试的请求,直接抛出
    return Promise.reject(error);
  }
);

2. 优化上层catch块的错误提示

在handleLike的catch块中,避免重复弹出401错误:

catch (e) {
  // 仅当不是401错误,或刷新已失败时才弹出提示
  if (e.response?.status !== 401 || e.response.data.message === "登录已过期,请重新登录") {
    console.log("Something went wrong while liking video", e);
    toast.error(e.response?.data?.message || "操作失败");
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 21:40:35