refactor(frontend): replace axios with the native Fetch API

Drop the axios (and qs) dependencies in favor of a native fetch wrapper.
axios only ever handled same-origin JSON/form calls, a CSRF header, a 401
redirect, and a 403-retry, all of which the platform now provides directly.

- New src/api/http-init.ts (replaces axios-init.ts) reimplements the
  request/response interceptors on fetch: base-path prefixing,
  X-Requested-With, same-origin credentials, the CSRF token on unsafe
  methods, a single 403 retry with token refresh, and the 401
  redirect-and-latch. A small encodeForm() reproduces qs's
  arrayFormat:'repeat' encoding, so the request wire format is unchanged.
- HttpUtil (src/utils/index.ts) keeps its public signatures and the Msg
  envelope, so the ~49 API call sites are untouched. HttpOptions is now
  hand-rolled instead of extending AxiosRequestConfig.
- PanelUpdateModal drops its lone direct axios.get in favor of HttpUtil.get
  with { silent, timeout }.
- Add tests for the fetch core (CSRF header, form/JSON/FormData bodies,
  base-path prefix, 403 retry, 401 redirect, tolerant body parse) and for
  HttpUtil's envelope unwrap / toast / error mapping; this logic was
  previously untested.
- Remove the vendor-axios manualChunks branch and the qs type shim, and
  reword stale "axios" mentions in docs and route comments.
This commit is contained in:
MHSanaei
2026-07-08 01:09:18 +02:00
parent 15faec6258
commit bc309ed9f8
21 changed files with 521 additions and 203 deletions
+14 -10
View File
@@ -1,6 +1,6 @@
import axios from 'axios';
import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import i18next from 'i18next';
import { httpRequest } from '@/api/http-init';
import type { HttpResponse } from '@/api/http-init';
import { getMessage } from './messageBus';
type RespEnvelope = { success?: unknown; msg?: unknown; obj?: unknown };
@@ -17,7 +17,11 @@ export class Msg<T = unknown> {
}
}
export interface HttpOptions extends AxiosRequestConfig {
export interface HttpOptions {
headers?: Record<string, string> | Headers;
params?: unknown;
timeout?: number;
signal?: AbortSignal;
silent?: boolean;
silentSuccess?: boolean;
}
@@ -48,7 +52,7 @@ export class HttpUtil {
getMessage().error(msg.msg);
}
static _respToMsg(resp: AxiosResponse | undefined): Msg {
static _respToMsg(resp: HttpResponse | undefined): Msg {
if (!resp || !resp.data) {
return new Msg(false, 'No response data');
}
@@ -64,15 +68,15 @@ export class HttpUtil {
}
static async get<T = unknown>(url: string, params?: unknown, options: HttpOptions = {}): Promise<Msg<T>> {
const { silent, silentSuccess, ...axiosOpts } = options;
const { silent, silentSuccess, ...rest } = options;
try {
const resp = await axios.get(url, { params, ...axiosOpts });
const resp = await httpRequest('GET', url, undefined, { ...rest, params });
const msg = this._respToMsg(resp) as Msg<T>;
if (!silent) this._handleMsg(msg, silentSuccess);
return msg;
} catch (error) {
console.error('GET request failed:', error);
const err = error as AxiosError<{ message?: string }>;
const err = error as { response?: { data?: { message?: string } }; message?: string };
const errorMsg = new Msg<T>(false, err.response?.data?.message || err.message || 'Request failed');
if (!silent) this._handleMsg(errorMsg);
return errorMsg;
@@ -80,15 +84,15 @@ export class HttpUtil {
}
static async post<T = unknown>(url: string, data?: unknown, options: HttpOptions = {}): Promise<Msg<T>> {
const { silent, silentSuccess, ...axiosOpts } = options;
const { silent, silentSuccess, ...rest } = options;
try {
const resp = await axios.post(url, data, axiosOpts);
const resp = await httpRequest('POST', url, data, rest);
const msg = this._respToMsg(resp) as Msg<T>;
if (!silent) this._handleMsg(msg, silentSuccess);
return msg;
} catch (error) {
console.error('POST request failed:', error);
const err = error as AxiosError<{ message?: string }>;
const err = error as { response?: { data?: { message?: string } }; message?: string };
const errorMsg = new Msg<T>(false, err.response?.data?.message || err.message || 'Request failed');
if (!silent) this._handleMsg(errorMsg);
return errorMsg;