-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Description
🧐 问题描述 | Problem description
Originally I would use the response interceptors to intercept the response, detect the 401 and trigger a new request to generate a new access token via refresh and then retry the initial request and respond accordingly.. this way, the original requester will wait on the resp instead of going into the catch (via try/catch).
Issue is, only 2XX status codes are triggering the response interceptor? Therefore, I need to rely on the errorHandler and I am facing weird timing issues whereby the initial request is getting the original 401 response, causing it to except (go into catch), then once that completes it goes back and gets my refresh and my logic that should have fired before the catch is firing after! Very strange behaviour. Looking for support on how to correctly implement this. All original solutions I could locate, all used the response interceptor. I also use to use the response interceptor. So not sure if this is a bug or intended behaviour now with the latest version but I cant for the life of me, figure this out.
💻 示例代码 | Sample code
In my app.tsx:
/**
* @see https://umijs.org/docs/api/runtime-config#getinitialstate
* */
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
let currentUser: API.CurrentUser | undefined;
let loading = true;
const fetchUserInfo = async () => {
try {
const msg = await getCurrentUser(); // triggers refresh if expired
return msg.data;
} catch (_error) {
// ISSUE HERE, we except before the interception finishes!
return undefined; // fail silently
} finally {
loading = false; // stop loading when done
}
};
const {location} = history;
if (![loginPath, '/user/register', '/user/register-result'].includes(location.pathname)) {
currentUser = await fetchUserInfo();
}
return {
fetchUserInfo,
currentUser,
loading,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
/**
* @name request configuration, can configure error handling
* It provides a unified network request and error handling solution based on axios and a hooks' useRequest.
* @doc https://umijs.org/docs/max/request#configuration
*/
export const request: RequestConfig = {
baseURL: '/',
...errorConfig
};
// errorConfig (don't mind the refresh token in local storage, just for testing purposes!):
import type {RequestOptions} from '@@/plugin-request/request';
import {history, request, RequestConfig} from '@umijs/max';
import {message, notification} from 'antd';
import {refreshAccessToken} from "@/services/user/auth";
// Error handling scheme: error types
enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3,
REDIRECT = 9,
}
// Response data format agreed with the backend
interface ResponseStructure {
success: boolean;
data: any;
errorCode?: number;
errorMessage?: string;
showType?: ErrorShowType;
}
// Track refresh state globally
let isRefreshing = false;
let refreshPromise: Promise<string | null> | null = null;
let pendingQueue: ((token: string | null) => void)[] = [];
// Helper: run queued requests once refresh completes
const processQueue = (token: string | null) => {
pendingQueue.forEach((cb) => cb(token));
pendingQueue = [];
};
/**
* Error handling
* Built-in error handling of pro, you can make your own changes here
* @doc https://umijs.org/docs/max/request#configuration
*/
export const errorConfig: RequestConfig = {
// Error handling: error handling scheme of umi@3.
errorConfig: {
// Error thrower
errorThrower: (res) => {
const {success, errorMessage} = res as ResponseStructure;
// Only throw for business errors (success = false), not for HTTP status errors.
if (!success) {
const error: any = new Error(errorMessage);
error.name = 'BizError';
error.info = res;
throw error;
}
// Let HTTP errors be handled downstream by errorHandler
},
//Error receiver and handler
errorHandler: async (error: any, opts: any) => {
if (opts?.skipErrorHandler) throw error;
// Error thrown by our errorThrower.
if (error.name === 'BizError') {
const errorInfo: ResponseStructure | undefined = error.info;
if (errorInfo) {
const {errorMessage, errorCode} = errorInfo;
switch (errorInfo.showType) {
case ErrorShowType.SILENT:
// do nothing
break;
case ErrorShowType.WARN_MESSAGE:
void message.warning(errorMessage);
break;
case ErrorShowType.ERROR_MESSAGE:
void message.error(errorMessage);
break;
case ErrorShowType.NOTIFICATION:
notification.open({
description: errorMessage,
message: errorCode,
});
break;
case ErrorShowType.REDIRECT:
// TODO: redirect
break;
default:
void message.error(errorMessage);
}
}
return;
}
// Handle HTTP errors
if (error.response) {
const {status, config} = error.response;
if (status === 401) {
// We remove access_token so we do not override our request to refresh token endpoint
// (since we use the refresh token in header)
localStorage.removeItem('access_token');
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
if (history.location.pathname !== '/user/login') {
history.push('/user/login');
}
return Promise.reject(error); // ensure caller sees rejection
}
// If already refreshing, queue this request
if (isRefreshing && refreshPromise) {
return new Promise<any>((resolve, reject) => {
pendingQueue.push((newToken) => {
if (!newToken) {
reject(error); // refresh failed
return;
}
request(config.url!, {
...config,
skipErrorHandler: true,
})
.then(resolve)
.catch(reject);
});
});
}
// First failing request triggers the refresh
if (!refreshPromise) {
isRefreshing = true;
refreshPromise = refreshAccessToken(refreshToken)
.then(data => {
if (data.access_token) {
localStorage.setItem('access_token', data.access_token);
processQueue(data.access_token);
return data.access_token;
} else {
processQueue(null);
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
if (history.location.pathname !== '/user/login') {
history.push('/user/login');
}
return null;
}
})
.catch(() => {
processQueue(null);
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
if (history.location.pathname !== '/user/login') {
history.push('/user/login');
}
return null;
})
.finally(() => {
isRefreshing = false;
refreshPromise = null;
});
}
// Wait for the refresh to complete
const newToken = await refreshPromise;
if (!newToken) return Promise.reject(error);
// Retry original request
return request(config.url!, {
...config,
skipErrorHandler: true,
});
} else if (status === 403) {
// Do nothing for 403
return;
}
// other non-2xx HTTP codes
return;
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
void message.error('No response received! Please retry.');
} else {
// Something happened in setting up the request that triggered an Error
void message.error('Request error, please retry.');
}
}
},
// Request interceptors
requestInterceptors: [
(config: RequestOptions) => {
// Allow per-request `prefix` override
// If config.prefix is present (set from API call), use it to rebuild URL
if (config.prefix) {
config.url = `${config.prefix}${config.url}`;
delete config.prefix; // Prevent double prefixing later
}
// Always inject latest access token
const token = localStorage.getItem('access_token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
}
],
// Response interceptors
// Only triggers for 2xx HTTP status codes
responseInterceptors: [
(response) => {
const resData = response.data as ResponseStructure;
// Handle backend errors
if (resData?.success === false) {
void message.error(resData.errorMessage || 'Request failed!');
}
return response;
},
],
};
🚑 其他信息 | Other information
OS: Mac OS Sequoia V15.5
Node:v20.19.4
浏览器 | browser:Google Chrome: Version 139.0.7258.139 (Official Build) (arm64)