File size: 4,094 Bytes
85270f9 f6daea4 85270f9 3241f40 85270f9 3241f40 85270f9 3241f40 85270f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | import axios, {AxiosError, type AxiosRequestConfig} from "axios";
import webConfig from "@/constants/common-env";
import {clearStoredAuthSession, getStoredAuthKey} from "@/store/auth";
type RequestConfig = AxiosRequestConfig & {
redirectOnUnauthorized?: boolean;
};
type ErrorPayload = {
detail?: string | { error?: string | { message?: string } };
error?: string | { message?: string };
message?: string;
};
function errorMessageFromValue(value: unknown): string {
if (typeof value === "string") {
return value;
}
if (!value || typeof value !== "object") {
return "";
}
const item = value as { error?: unknown; message?: unknown };
if (typeof item.message === "string") {
return item.message;
}
return errorMessageFromValue(item.error);
}
export const request = axios.create({
baseURL: webConfig.apiUrl.replace(/\/$/, ""),
// 浏览器里 XHR 通道会撞上 net::ERR_TOO_MANY_REDIRECTS:同一个地址用 fetch
// 能正常拿到 401/200,curl 也是 200 且零跳转,只有走 axios 默认的 XHR 适配器
// 时才在浏览器内部被反复重定向。服务端没有 middleware、也没有任何 30x,
// 所以这不是接口问题。改用 fetch 适配器,请求直接走 fetch,绕开 XHR 那条路。
// 代价:不支持 onUploadProgress(项目里未使用)。
adapter: "fetch",
});
request.interceptors.request.use(async (config) => {
const nextConfig = {...config};
const authKey = await getStoredAuthKey();
const headers = {...(nextConfig.headers || {})} as Record<string, string>;
if (authKey && !headers.Authorization) {
headers.Authorization = `Bearer ${authKey}`;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
nextConfig.headers = headers;
return nextConfig;
});
request.interceptors.response.use(
(response) => response,
async (error: AxiosError<ErrorPayload>) => {
const status = error.response?.status;
const shouldRedirect = (error.config as RequestConfig | undefined)?.redirectOnUnauthorized !== false;
if (status === 401 && shouldRedirect && typeof window !== "undefined") {
// Avoid redirect loop — only redirect if not already on /login
if (!window.location.pathname.startsWith("/login")) {
await clearStoredAuthSession();
window.location.replace("/login");
// Return a never-resolving promise to prevent further error handling
// while the browser navigates away
return new Promise(() => {});
}
}
let payload = error.response?.data;
if (typeof Blob !== "undefined" && payload instanceof Blob) {
try {
const text = await payload.text();
payload = JSON.parse(text) as ErrorPayload;
} catch {
// Keep the original Blob when the response is not JSON.
}
}
const message =
errorMessageFromValue(payload?.detail) ||
errorMessageFromValue(payload?.error) ||
payload?.message ||
error.message ||
`请求失败 (${status || 500})`;
const nextError = new Error(message) as Error & { status?: number };
nextError.status = status;
return Promise.reject(nextError);
},
);
type RequestOptions = {
method?: string;
body?: unknown;
headers?: Record<string, string>;
redirectOnUnauthorized?: boolean;
signal?: AbortSignal;
timeout?: number;
};
export async function httpRequest<T>(path: string, options: RequestOptions = {}) {
const {method = "GET", body, headers, redirectOnUnauthorized = true, signal, timeout} = options;
const config: RequestConfig = {
url: path,
method,
data: body,
headers,
redirectOnUnauthorized,
signal,
timeout,
};
const response = await request.request<T>(config);
return response.data;
}
|