Axios Response 拦截器的坑:为何明明登录了却仍然 401?
在使用 axios
进行前后端交互时,response
拦截器是一个常用的工具,能够帮助我们在收到服务器响应后进行统一处理,比如自动刷新 token
、格式化数据或者全局错误处理。然而,axios
的 response
拦截器有一个不易察觉的“坑”——拦截器的执行时机是 resolve 之后
,这可能导致在业务逻辑已经认为登录成功的情况下,紧接着的请求却仍然返回 401
,给开发者带来极大的困惑。
看似正确的 token 处理方式
很多开发者在处理 token
时,会采用类似下面的 axios
拦截器逻辑:
axios.interceptors.response.use(
response => {
if (response.data.token) {
localStorage.setItem("token", response.data.token);
}
return response;
},
async error => {
if (error.response.status === 401) {
const newToken = await refreshToken();
localStorage.setItem("token", newToken);
return axios(error.config); // 重新发送原请求
}
return Promise.reject(error);
}
);
表面上看,这段代码似乎没有问题:
- 如果服务器返回了新的 token,就更新本地存储中的 token。
- 如果遇到 401,就刷新 token,然后重新发起请求。
但是,当你真正运行代码后,可能会发现一个奇怪的现象:
明明已经登录成功了,为什么接下来的请求仍然是 401?
问题的本质:拦截器比 resolve 慢一步
拦截器的执行时机
axios
的 response
拦截器并不会立即生效,而是在 Promise.resolve(response)
之后才会执行。这意味着:
- 业务代码在 await axios.post("/login") 返回后,认为自己已经完成了登录,直接开始执行后续请求。
- 但是此时 token 可能还没有更新,因为 response 拦截器尚未执行。
- 于是,后续的请求仍然携带的是旧的、失效的 token,导致服务器返回 401。
示例:问题复现
假设我们的登录逻辑如下:
async function login() {
const res = await axios.post("/login", { username, password });
console.log("登录成功,token:", localStorage.getItem("token"));
fetchUserData(); // 立刻发起请求
}
function fetchUserData() {
axios.get("/user/profile", {
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` }
}).then(res => {
console.log("用户信息:", res.data);
});
}
可能的执行顺序:
- axios.post("/login") 发起请求,服务器返回 token。
- await axios.post("/login") 解析 response,业务代码继续执行。
- fetchUserData() 立刻执行,获取 token 并发送请求。
- 此时拦截器还未执行,token 仍然是旧的或 null,请求失败,返回 401。
这样,登录之后的第一个 API 请求可能失败,导致一些奇怪的 Bug,例如:
- 登录后页面刷新失败(因为数据请求 401)。
- 需要重新登录才能成功访问用户数据。
这个坑有多隐蔽?
这个问题极其隐蔽,原因如下:
- 时机上的细微差异:拦截器的执行虽然是异步的,但其延迟足以导致问题。
- 大多数情况下可能正常:如果 token 过期时间长,或者后续 API 请求稍微晚一点,问题就不会发生,因此它通常只在某些特定情况下触发。
- 无法直观看到 token 更新的时间点:即使 console.log(localStorage.getItem("token")) 也可能误导你,让你以为 token 早已更新。
误区:用 then() 处理拦截器
一些开发者可能会尝试用 .then()
确保 token
已更新:
axios.post("/login", { username, password }).then(() => {
fetchUserData();
});
但这并不能解决问题,因为 then()
只是在 response
解析完之后执行,并不能保证拦截器已经完成 token
的更新。
如何正确认知这个坑?
核心点:拦截器是在 resolve 之后执行的,而不是在 axios 响应返回时立即生效。
因此,开发者在使用 axios
拦截器处理 token
时,应该始终考虑:
- 后续请求是否可能比 token 更新更早发出?
- token 逻辑是否真正等待拦截器更新完成?
结语
Axios 的 response
拦截器提供了强大的功能,但它的执行时机往往容易被误解,导致在业务代码认为 token 已更新时,实际还未更新
,最终造成 401
问题。理解这一点,可以帮助你更好地设计 token
更新逻辑,避免不必要的 Bug。