mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
add guards
This commit is contained in:
parent
9758fc4c19
commit
1180d48dcd
@ -1,3 +1,4 @@
|
||||
// frontend/src/services/http.ts
|
||||
import axios from 'axios';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import { alert } from '../components/toast';
|
||||
@ -5,6 +6,11 @@ import { broadcastErroredFiles, extractErrorFileIds, normalizeAxiosErrorData } f
|
||||
import { showSpecialErrorToast } from './specialErrorToasts';
|
||||
|
||||
const FRIENDLY_FALLBACK = 'There was an error processing your request.';
|
||||
const MAX_TOAST_BODY_CHARS = 400; // avoid massive, unreadable toasts
|
||||
|
||||
function clampText(s: string, max = MAX_TOAST_BODY_CHARS): string {
|
||||
return s && s.length > max ? `${s.slice(0, max)}…` : s;
|
||||
}
|
||||
|
||||
function isUnhelpfulMessage(msg: string | null | undefined): boolean {
|
||||
const s = (msg || '').trim();
|
||||
@ -74,7 +80,7 @@ function extractAxiosErrorMessage(error: any): { title: string; body: string } {
|
||||
}
|
||||
}
|
||||
|
||||
// Create axios instance with default config similar to SaaS client
|
||||
// ---------- Axios instance creation ----------
|
||||
const __globalAny = (typeof window !== 'undefined' ? (window as any) : undefined);
|
||||
|
||||
type ExtendedAxiosInstance = AxiosInstance & {
|
||||
@ -82,8 +88,8 @@ type ExtendedAxiosInstance = AxiosInstance & {
|
||||
isCancel: typeof axios.isCancel;
|
||||
};
|
||||
|
||||
// Reuse existing client across HMR reloads to avoid duplicate interceptors
|
||||
const __PREV_CLIENT: ExtendedAxiosInstance | undefined = __globalAny?.__SPDF_HTTP_CLIENT as ExtendedAxiosInstance | undefined;
|
||||
const __PREV_CLIENT: ExtendedAxiosInstance | undefined =
|
||||
__globalAny?.__SPDF_HTTP_CLIENT as ExtendedAxiosInstance | undefined;
|
||||
|
||||
let __createdClient: any;
|
||||
if (__PREV_CLIENT) {
|
||||
@ -91,79 +97,125 @@ if (__PREV_CLIENT) {
|
||||
} else if (typeof (axios as any)?.create === 'function') {
|
||||
try {
|
||||
__createdClient = (axios as any).create();
|
||||
} catch (_e) {
|
||||
} catch (e) {
|
||||
console.debug('createClient', e);
|
||||
__createdClient = axios as any;
|
||||
}
|
||||
} else {
|
||||
__createdClient = axios as any;
|
||||
}
|
||||
|
||||
const apiClient: ExtendedAxiosInstance = (__createdClient || (axios as any)) as ExtendedAxiosInstance;
|
||||
|
||||
// Augment instance with axios static helpers for backwards compatibility
|
||||
if (apiClient) {
|
||||
try { (apiClient as any).CancelToken = (axios as any).CancelToken; } catch (_e) { void _e; }
|
||||
try { (apiClient as any).isCancel = (axios as any).isCancel; } catch (_e) { void _e; }
|
||||
try { (apiClient as any).CancelToken = (axios as any).CancelToken; } catch (e) { console.debug('setCancelToken', e); }
|
||||
try { (apiClient as any).isCancel = (axios as any).isCancel; } catch (e) { console.debug('setIsCancel', e); }
|
||||
}
|
||||
|
||||
// Install Axios response error interceptor on the instance (guard against double-registration in HMR)
|
||||
// ---------- Base defaults ----------
|
||||
try {
|
||||
const env = (import.meta as any)?.env || {};
|
||||
apiClient.defaults.baseURL = env?.VITE_API_BASE_URL ?? '/';
|
||||
apiClient.defaults.responseType = 'json';
|
||||
// If OSS relies on cookies, uncomment:
|
||||
// apiClient.defaults.withCredentials = true;
|
||||
// Sensible timeout to avoid “forever hanging”:
|
||||
apiClient.defaults.timeout = 20000;
|
||||
} catch (e) {
|
||||
console.debug('setDefaults', e);
|
||||
apiClient.defaults.baseURL = apiClient.defaults.baseURL || '/';
|
||||
apiClient.defaults.responseType = apiClient.defaults.responseType || 'json';
|
||||
apiClient.defaults.timeout = apiClient.defaults.timeout || 20000;
|
||||
}
|
||||
|
||||
// ---------- Install a single response error interceptor (dedup + UX) ----------
|
||||
if (__globalAny?.__SPDF_HTTP_ERR_INTERCEPTOR_ID !== undefined && __PREV_CLIENT) {
|
||||
try { __PREV_CLIENT.interceptors.response.eject(__globalAny.__SPDF_HTTP_ERR_INTERCEPTOR_ID); } catch (_e) { void _e; }
|
||||
try {
|
||||
__PREV_CLIENT.interceptors.response.eject(__globalAny.__SPDF_HTTP_ERR_INTERCEPTOR_ID);
|
||||
} catch (e) {
|
||||
console.debug('ejectInterceptor', e);
|
||||
}
|
||||
}
|
||||
|
||||
const __recentSpecialByEndpoint: Record<string, number> = (__globalAny?.__SPDF_RECENT_SPECIAL || {});
|
||||
const __SPECIAL_SUPPRESS_MS = 1500; // brief window to suppress generic duplicate after special toast
|
||||
const __INTERCEPTOR_ID__ = apiClient?.interceptors?.response?.use ? apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const { title, body } = extractAxiosErrorMessage(error);
|
||||
// If server sends structured file IDs for failures, also mark them errored in UI
|
||||
try {
|
||||
|
||||
const __INTERCEPTOR_ID__ = apiClient?.interceptors?.response?.use
|
||||
? apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
// Compute title/body (friendly) from the error object
|
||||
const { title, body } = extractAxiosErrorMessage(error);
|
||||
|
||||
// Normalize response data ONCE, reuse for both ID extraction and special-toast matching
|
||||
const raw = (error?.response?.data) as any;
|
||||
const data = await normalizeAxiosErrorData(raw);
|
||||
const ids = extractErrorFileIds(data);
|
||||
if (ids && ids.length > 0) {
|
||||
broadcastErroredFiles(ids);
|
||||
}
|
||||
} catch (_e) { void _e; }
|
||||
let normalized: unknown = raw;
|
||||
try { normalized = await normalizeAxiosErrorData(raw); } catch (e) { console.debug('normalizeAxiosErrorData', e); }
|
||||
|
||||
// Generic-vs-special dedupe by endpoint
|
||||
const url: string | undefined = error?.config?.url;
|
||||
const status: number | undefined = error?.response?.status;
|
||||
const now = Date.now();
|
||||
const isSpecial = status === 422 || /Failed files:/.test(body) || /invalid\/corrupted file\(s\)/i.test(body);
|
||||
if (isSpecial && url) {
|
||||
__recentSpecialByEndpoint[url] = now;
|
||||
if (__globalAny) __globalAny.__SPDF_RECENT_SPECIAL = __recentSpecialByEndpoint;
|
||||
}
|
||||
if (!isSpecial && url) {
|
||||
const last = __recentSpecialByEndpoint[url] || 0;
|
||||
if (now - last < __SPECIAL_SUPPRESS_MS) {
|
||||
return Promise.reject(error);
|
||||
// 1) If server sends structured file IDs for failures, also mark them errored in UI
|
||||
try {
|
||||
const ids = extractErrorFileIds(normalized);
|
||||
if (ids && ids.length > 0) {
|
||||
broadcastErroredFiles(ids);
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('extractErrorFileIds', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Show specialized friendly toasts if matched; otherwise show the generic one
|
||||
const raw = (error?.response?.data) as any;
|
||||
let rawString: string | undefined;
|
||||
try {
|
||||
if (typeof raw === 'string') rawString = raw;
|
||||
else rawString = await normalizeAxiosErrorData(raw).then((d) => (typeof d === 'string' ? d : JSON.stringify(d)));
|
||||
} catch { /* ignore */ }
|
||||
const handled = showSpecialErrorToast(rawString, { status });
|
||||
if (!handled) {
|
||||
alert({ alertType: 'error', title, body, expandable: true, isPersistentPopup: false });
|
||||
// 2) Generic-vs-special dedupe by endpoint
|
||||
const url: string | undefined = error?.config?.url;
|
||||
const status: number | undefined = error?.response?.status;
|
||||
const now = Date.now();
|
||||
const isSpecial =
|
||||
status === 422 ||
|
||||
status === 409 || // often actionable conflicts
|
||||
/Failed files:/.test(body) ||
|
||||
/invalid\/corrupted file\(s\)/i.test(body);
|
||||
|
||||
if (isSpecial && url) {
|
||||
__recentSpecialByEndpoint[url] = now;
|
||||
if (__globalAny) __globalAny.__SPDF_RECENT_SPECIAL = __recentSpecialByEndpoint;
|
||||
}
|
||||
if (!isSpecial && url) {
|
||||
const last = __recentSpecialByEndpoint[url] || 0;
|
||||
if (now - last < __SPECIAL_SUPPRESS_MS) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Show specialized friendly toasts if matched; otherwise show the generic one
|
||||
let rawString: string | undefined;
|
||||
try {
|
||||
rawString =
|
||||
typeof normalized === 'string'
|
||||
? normalized
|
||||
: JSON.stringify(normalized);
|
||||
} catch (e) {
|
||||
console.debug('extractErrorFileIds', e);
|
||||
}
|
||||
|
||||
const handled = showSpecialErrorToast(rawString, { status });
|
||||
if (!handled) {
|
||||
const displayBody = clampText(body);
|
||||
alert({ alertType: 'error', title, body: displayBody, expandable: true, isPersistentPopup: false });
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
) : undefined as any;
|
||||
)
|
||||
: undefined as any;
|
||||
|
||||
if (__globalAny) {
|
||||
__globalAny.__SPDF_HTTP_ERR_INTERCEPTOR_ID = __INTERCEPTOR_ID__;
|
||||
__globalAny.__SPDF_RECENT_SPECIAL = __recentSpecialByEndpoint;
|
||||
__globalAny.__SPDF_HTTP_CLIENT = apiClient;
|
||||
}
|
||||
|
||||
// ---------- Fetch helper ----------
|
||||
export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
||||
const res = await fetch(input, { credentials: init?.credentials ?? 'include', ...init });
|
||||
|
||||
if (!res.ok) {
|
||||
let detail = '';
|
||||
try {
|
||||
@ -177,14 +229,19 @@ export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Pr
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
|
||||
const title = titleForStatus(res.status);
|
||||
const body = isUnhelpfulMessage(detail || res.statusText) ? FRIENDLY_FALLBACK : (detail || res.statusText);
|
||||
alert({ alertType: 'error', title, body, expandable: true, isPersistentPopup: false });
|
||||
alert({ alertType: 'error', title, body: clampText(body), expandable: true, isPersistentPopup: false });
|
||||
|
||||
// Important: match Axios semantics so callers can try/catch
|
||||
throw new Error(body || res.statusText);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
// ---------- Convenience API surface and exports ----------
|
||||
export const api = {
|
||||
get: apiClient.get,
|
||||
post: apiClient.post,
|
||||
@ -196,5 +253,3 @@ export const api = {
|
||||
|
||||
export default apiClient;
|
||||
export type { CancelTokenSource } from 'axios';
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user