# Frontend: API Client и Error Handling **Контекст**: В `frontend/src/lib/api.ts` реализован простой wrapper над `fetch`. В `useAuth.tsx` используется для авторизации. ## Суть **Текущий API клиент имеет пробелы**: 1. Нет автоматического refresh токена при 401 2. Нет retry логики для сетевых ошибок 3. Нет типизированных ошибок по эндпоинтам 4. `credentials: 'include'` + `Authorization` header — потенциальный CSRF (хотя SameSite=Lax помогает) 5. Нет request/response interceptors для логирования, метрик ## Код (проблемные места) ```typescript // frontend/src/lib/api.ts:15-26 const res = await fetch(`${API_URL}${path}`, { ...options, headers, credentials: 'include', // Куки + Bearer токен одновременно }) if (!res.ok) { const error = await res.json().catch(() => ({ detail: res.statusText })) throw new ApiRequestError(res.status, error.detail || 'Unknown error', error) } ``` ```tsx // frontend/src/hooks/useAuth.tsx:30-40 const refreshSession = useCallback(async () => { try { const res = await api.post<{ access_token: string }>('/auth/refresh') setTokens(res.access_token) const userRes = await api.get('/auth/me') setUser(userRes) } catch { clearTokens() setUser(null) } }, [setTokens, clearTokens]) // Проблема: refresh вызывается при каждом монтировании, нет защиты от race condition ``` ## Решение: Улучшенный API клиент ### 1. Автоматический refresh при 401 (с защитой от race condition) ```typescript // frontend/src/lib/api.ts let refreshPromise: Promise | null = null async function refreshAccessToken(): Promise { if (refreshPromise) return refreshPromise refreshPromise = (async () => { const res = await fetch(`${API_URL}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', // Refresh token в HttpOnly куке }) if (!res.ok) throw new Error('Refresh failed') const data = await res.json() return data.access_token })() try { return await refreshPromise } finally { refreshPromise = null } } async function request(path: string, options: RequestInit = {}): Promise { const token = getAccessToken() const headers = { 'Content-Type': 'application/json', ...options.headers } if (token) headers['Authorization'] = `Bearer ${token}` const res = await fetch(`${API_URL}${path}`, { ...options, headers, credentials: 'include' }) if (res.status === 401 && !path.includes('/auth/')) { // Автоматический refresh и retry const newToken = await refreshAccessToken() headers['Authorization'] = `Bearer ${newToken}` const retryRes = await fetch(`${API_URL}${path}`, { ...options, headers, credentials: 'include' }) if (!retryRes.ok) throw await parseError(retryRes) return retryRes.json() } if (!res.ok) throw await parseError(res) return res.json() } ``` ### 2. HttpOnly куки для refresh token (безопаснее localStorage) **Backend изменения** (в `auth.go`): - При login/register: `Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000` - При refresh: читать из куки, выдавать новый access + ротировать refresh - При logout: `Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0` **Frontend**: убрать `localStorage` для refresh, использовать `credentials: 'include'` ### 3. Типизированные ошибки по эндпоинтам ```typescript // frontend/src/types/api.ts export interface ApiErrorResponse { type: string title: string status: number detail: string errors?: { field: string; message: string }[] } export class ApiError extends Error { constructor(public response: ApiErrorResponse) { super(response.detail) this.name = 'ApiError' } } ``` ## Альтернативы | Подход | Плюсы | Минусы | |--------|-------|--------| | **Улучшенный fetch wrapper (рекомендую)** | Без зависимостей, полный контроль | Нужно писать boilerplate | | **TanStack Query (React Query)** | Кэширование, deduping, retry, devtools | Зависимость, кривая обучения | | **SWR** | Легкий, кэширование | Меньше фич чем TanStack Query | | **Axios** | Interceptors, привычный API | Устаревший, тяжелый, не нужен в 2024 | ## Связанные заметки - [[backend-auth-security]] — HttpOnly куки для refresh token - [[frontend-auth-patterns]] - [[architecture-overview]] ## Источник Задача: Code review PhotoPlaces — улучшение API клиента и auth flow ## Теги #frontend #api-client #auth #react #typescript #best-practice