frontend-api-client.md 5.3 KB

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 для логирования, метрик

Код (проблемные места)

// 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)
}
// 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<User>('/auth/me')
        setUser(userRes)
    } catch {
        clearTokens()
        setUser(null)
    }
}, [setTokens, clearTokens])
// Проблема: refresh вызывается при каждом монтировании, нет защиты от race condition

Решение: Улучшенный API клиент

1. Автоматический refresh при 401 (с защитой от race condition)

// frontend/src/lib/api.ts
let refreshPromise: Promise<string> | null = null

async function refreshAccessToken(): Promise<string> {
    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<T>(path: string, options: RequestInit = {}): Promise<T> {
    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. Типизированные ошибки по эндпоинтам

// 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