Skip to content

Commit 62a357a

Browse files
committed
api interceptors simpler functions
1 parent 71055a4 commit 62a357a

File tree

1 file changed

+70
-62
lines changed

1 file changed

+70
-62
lines changed

frontend/src/lib/api-interceptors.ts

Lines changed: 70 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,27 @@ import type { ValidationError } from './api';
1515
let isHandling401 = false;
1616
const AUTH_ENDPOINTS = ['/api/v1/auth/login', '/api/v1/auth/register', '/api/v1/auth/verify-token'];
1717

18+
type ToastType = 'error' | 'warning' | 'info' | 'success';
19+
20+
const STATUS_MESSAGES: Record<number, { message: string; type: ToastType }> = {
21+
403: { message: 'Access denied.', type: 'error' },
22+
429: { message: 'Too many requests. Please slow down.', type: 'warning' },
23+
};
24+
25+
function extractDetail(err: unknown): string | ValidationError[] | null {
26+
if (typeof err === 'object' && err !== null && 'detail' in err) {
27+
return (err as { detail: string | ValidationError[] }).detail;
28+
}
29+
return null;
30+
}
31+
1832
export function getErrorMessage(err: unknown, fallback = 'An error occurred'): string {
1933
if (!err) return fallback;
2034

21-
if (typeof err === 'object' && 'detail' in err) {
22-
const detail = (err as { detail?: ValidationError[] | string }).detail;
23-
if (typeof detail === 'string') return detail;
24-
if (Array.isArray(detail) && detail.length > 0) {
25-
return detail.map((e) => `${e.loc[e.loc.length - 1]}: ${e.msg}`).join(', ');
26-
}
35+
const detail = extractDetail(err);
36+
if (typeof detail === 'string') return detail;
37+
if (Array.isArray(detail) && detail.length > 0) {
38+
return detail.map((e) => `${e.loc[e.loc.length - 1]}: ${e.msg}`).join(', ');
2739
}
2840

2941
if (err instanceof Error) return err.message;
@@ -49,12 +61,56 @@ function clearAuthState(): void {
4961
sessionStorage.removeItem('authState');
5062
}
5163

52-
function handleAuthFailure(currentPath: string): void {
53-
clearAuthState();
54-
if (currentPath !== '/login' && currentPath !== '/register') {
55-
sessionStorage.setItem('redirectAfterLogin', currentPath);
64+
function handle401(isAuthEndpoint: boolean): void {
65+
if (isAuthEndpoint) return;
66+
67+
const wasAuthenticated = get(isAuthenticated);
68+
if (wasAuthenticated && !isHandling401) {
69+
isHandling401 = true;
70+
const currentPath = window.location.pathname + window.location.search;
71+
addToast('Session expired. Please log in again.', 'warning');
72+
clearAuthState();
73+
if (currentPath !== '/login' && currentPath !== '/register') {
74+
sessionStorage.setItem('redirectAfterLogin', currentPath);
75+
}
76+
goto('/login');
77+
setTimeout(() => { isHandling401 = false; }, 1000);
78+
} else {
79+
clearAuthState();
80+
}
81+
}
82+
83+
function handleErrorStatus(status: number | undefined, error: unknown, isAuthEndpoint: boolean): boolean {
84+
if (!status) {
85+
if (!isAuthEndpoint) addToast('Network error. Check your connection.', 'error');
86+
return true;
87+
}
88+
89+
if (status === 401) {
90+
handle401(isAuthEndpoint);
91+
return true;
92+
}
93+
94+
const mapped = STATUS_MESSAGES[status];
95+
if (mapped) {
96+
addToast(mapped.message, mapped.type);
97+
return true;
98+
}
99+
100+
if (status === 422) {
101+
const detail = extractDetail(error);
102+
if (Array.isArray(detail) && detail.length > 0) {
103+
addToast(`Validation error:\n${formatValidationErrors(detail)}`, 'error');
104+
return true;
105+
}
106+
}
107+
108+
if (status >= 500) {
109+
addToast('Server error. Please try again later.', 'error');
110+
return true;
56111
}
57-
goto('/login');
112+
113+
return false;
58114
}
59115

60116
export function initializeApiInterceptors(): void {
@@ -70,59 +126,11 @@ export function initializeApiInterceptors(): void {
70126

71127
console.error('[API Error]', { status, url, error });
72128

73-
// 401: Silent by default. Only show toast + redirect if user HAD an active session.
74-
if (status === 401) {
75-
if (isAuthEndpoint) {
76-
return error; // Auth endpoints handle their own messaging
77-
}
78-
const wasAuthenticated = get(isAuthenticated);
79-
if (wasAuthenticated && !isHandling401) {
80-
isHandling401 = true;
81-
try {
82-
const currentPath = window.location.pathname + window.location.search;
83-
addToast('Session expired. Please log in again.', 'warning');
84-
handleAuthFailure(currentPath);
85-
} finally {
86-
setTimeout(() => { isHandling401 = false; }, 1000);
87-
}
88-
} else {
89-
clearAuthState();
90-
}
91-
return error;
92-
}
93-
94-
if (status === 403) {
95-
addToast('Access denied.', 'error');
96-
return error;
97-
}
98-
99-
if (status === 422 && typeof error === 'object' && error !== null && 'detail' in error) {
100-
const detail = (error as { detail: ValidationError[] }).detail;
101-
if (Array.isArray(detail) && detail.length > 0) {
102-
addToast(`Validation error:\n${formatValidationErrors(detail)}`, 'error');
103-
return error;
104-
}
105-
}
106-
107-
if (status === 429) {
108-
addToast('Too many requests. Please slow down.', 'warning');
109-
return error;
110-
}
111-
112-
if (status && status >= 500) {
113-
addToast('Server error. Please try again later.', 'error');
114-
return error;
115-
}
116-
117-
if (!response && !isAuthEndpoint) {
118-
addToast('Network error. Check your connection.', 'error');
119-
return error;
120-
}
121-
122-
// Don't toast for auth-related silent failures
123-
if (!isAuthEndpoint) {
129+
const handled = handleErrorStatus(status, error, isAuthEndpoint);
130+
if (!handled && !isAuthEndpoint) {
124131
addToast(getErrorMessage(error, 'An error occurred'), 'error');
125132
}
133+
126134
return error;
127135
});
128136

0 commit comments

Comments
 (0)