216 lines
No EOL
7.1 KiB
TypeScript
216 lines
No EOL
7.1 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import api from '../api';
|
|
|
|
// Global request cache to prevent duplicate requests
|
|
const requestCache = new Map<string, Promise<any>>();
|
|
const cacheTimestamps = new Map<string, number>();
|
|
const CACHE_DURATION = 5000; // 5 seconds cache duration
|
|
|
|
// Generate cache key for request deduplication
|
|
function generateCacheKey(url: string, method: string, params?: Record<string, any>): 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<T> {
|
|
url: string;
|
|
method: 'get' | 'post' | 'put' | 'delete';
|
|
data?: T;
|
|
params?: Record<string, string | number | boolean>;
|
|
additionalConfig?: Record<string, any>; // For responseType, headers, etc.
|
|
}
|
|
|
|
// Hook for making API requests with consistent error handling
|
|
export function useApiRequest<RequestData = any, ResponseData = any>() {
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const request = useCallback(async ({
|
|
url,
|
|
method,
|
|
data,
|
|
params,
|
|
additionalConfig = {}
|
|
}: ApiRequestOptions<RequestData>): Promise<ResponseData> => {
|
|
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
|
|
};
|
|
}
|