import { useState, useCallback } from 'react'; import api from '../api'; // Global request cache to prevent duplicate requests const requestCache = new Map>(); const cacheTimestamps = new Map(); const CACHE_DURATION = 5000; // 5 seconds cache duration // Generate cache key for request deduplication function generateCacheKey(url: string, method: string, params?: Record): string { const paramsString = params ? JSON.stringify(params) : ''; return `${method.toUpperCase()}:${url}:${paramsString}`; } // Check if cached request is still valid function isCacheValid(cacheKey: string): boolean { const timestamp = cacheTimestamps.get(cacheKey); if (!timestamp) return false; return Date.now() - timestamp < CACHE_DURATION; } // Generic API error handling export function formatApiError(error: any, defaultMessage: string): string { if (error.response) { const data = error.response.data; // Handle FastAPI validation errors (detail is an array) if (data?.detail && Array.isArray(data.detail)) { return data.detail.map((err: any) => { if (typeof err === 'string') return err; if (err.msg) return `${err.loc?.join('.') || 'field'}: ${err.msg}`; return JSON.stringify(err); }).join(', '); } // Handle other error formats if (typeof data?.detail === 'string') return data.detail; if (typeof data?.message === 'string') return data.message; if (typeof data === 'string') return data; return defaultMessage; } else if (error.request) { return 'Keine Antwort vom Server erhalten'; } else { return error.message || defaultMessage; } } // Type for API request options export interface ApiRequestOptions { url: string; method: 'get' | 'post' | 'put' | 'delete'; data?: T; params?: Record; additionalConfig?: Record; // For responseType, headers, etc. } // Hook for making API requests with consistent error handling export function useApiRequest() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const request = useCallback(async ({ url, method, data, params, additionalConfig = {} }: ApiRequestOptions): Promise => { setIsLoading(true); setError(null); try { // Generate cache key for GET requests (only cache GET requests) const cacheKey = method === 'get' ? generateCacheKey(url, method, params) : null; // Check if we have a valid cached request for GET requests if (cacheKey && requestCache.has(cacheKey) && isCacheValid(cacheKey)) { console.log('🔧 useApiRequest: Using cached request', { url, method, cacheKey }); setIsLoading(false); return await requestCache.get(cacheKey)!; } console.log('🔧 useApiRequest: Making request', { url, method, hasData: !!data, hasParams: !!params, cacheKey, dataStructure: data ? { data, dataType: typeof data, dataKeys: Object.keys(data), dataEntries: Object.entries(data).map(([key, value]) => ({ key, value, valueType: typeof value, valueIsObject: typeof value === 'object' && value !== null && !Array.isArray(value), valueStringified: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value) })), dataStringified: JSON.stringify(data, null, 2) } : null, fullRequestConfig: { url, method, data, params, ...additionalConfig } }); // Create the request promise const requestPromise = api({ url, method, data, params, ...additionalConfig }).then(response => { console.log('🔧 useApiRequest: Request successful', { url, status: response.status, hasData: !!response.data }); // For blob responses, return the blob data directly if (additionalConfig.responseType === 'blob') { return response.data; } return response.data; }); // Cache GET requests if (cacheKey) { requestCache.set(cacheKey, requestPromise); cacheTimestamps.set(cacheKey, Date.now()); // Clean up old cache entries setTimeout(() => { if (requestCache.has(cacheKey) && !isCacheValid(cacheKey)) { requestCache.delete(cacheKey); cacheTimestamps.delete(cacheKey); } }, CACHE_DURATION + 1000); } const result = await requestPromise; setIsLoading(false); return result; } catch (error: any) { // Clear cache on error to allow retry const cacheKey = method === 'get' ? generateCacheKey(url, method, params) : null; if (cacheKey) { requestCache.delete(cacheKey); cacheTimestamps.delete(cacheKey); } console.log('🔧 useApiRequest: Request failed', { url, error: error.message, status: error.response?.status, statusText: error.response?.statusText, hasResponse: !!error.response, hasRequest: !!error.request, isAborted: error.code === 'ERR_CANCELED', authAuthority: sessionStorage.getItem('auth_authority'), hasCookies: document.cookie.includes('access_token') || document.cookie.includes('refresh_token') }); // Handle aborted requests specifically if (error.code === 'ERR_CANCELED' || error.message?.includes('aborted')) { const errorMessage = 'Request aborted'; setError(errorMessage); throw new Error(errorMessage); } // Handle authentication errors specifically if (error.response?.status === 401) { const errorMessage = 'Not authenticated'; setError(errorMessage); throw new Error(errorMessage); } // Handle rate limiting specifically if (error.response?.status === 429) { const errorMessage = error.response.data?.detail || error.response.data?.message || '30 per 1 minute'; setError(errorMessage); throw new Error(errorMessage); } const errorMessage = formatApiError(error, `Fehler bei ${method.toUpperCase()} ${url}`); setError(errorMessage); throw new Error(String(errorMessage)); // Ensure it's a string } finally { setIsLoading(false); } }, []); // Function to clear cache manually const clearCache = useCallback((url?: string, method?: string) => { if (url && method) { const cacheKey = generateCacheKey(url, method); requestCache.delete(cacheKey); cacheTimestamps.delete(cacheKey); console.log('🔧 useApiRequest: Cleared cache for', { url, method, cacheKey }); } else { requestCache.clear(); cacheTimestamps.clear(); console.log('🔧 useApiRequest: Cleared all cache'); } }, []); return { request, isLoading, error, clearCache }; }