diff --git a/.aider.tags.cache.v4/7c/53/4df34cfcfb07d9ce03ac76d43c69.val b/.aider.tags.cache.v4/7c/53/4df34cfcfb07d9ce03ac76d43c69.val new file mode 100644 index 000000000..bb648df0c Binary files /dev/null and b/.aider.tags.cache.v4/7c/53/4df34cfcfb07d9ce03ac76d43c69.val differ diff --git a/.aider.tags.cache.v4/af/d7/845f3ed21f516157272ae8608239.val b/.aider.tags.cache.v4/af/d7/845f3ed21f516157272ae8608239.val new file mode 100644 index 000000000..1b86627fa Binary files /dev/null and b/.aider.tags.cache.v4/af/d7/845f3ed21f516157272ae8608239.val differ diff --git a/.aider.tags.cache.v4/b7/3d/1a9c432bb6c69a466eb291476cc5.val b/.aider.tags.cache.v4/b7/3d/1a9c432bb6c69a466eb291476cc5.val new file mode 100644 index 000000000..bfb9e024f Binary files /dev/null and b/.aider.tags.cache.v4/b7/3d/1a9c432bb6c69a466eb291476cc5.val differ diff --git a/.aider.tags.cache.v4/cache.db b/.aider.tags.cache.v4/cache.db new file mode 100644 index 000000000..d8237564c Binary files /dev/null and b/.aider.tags.cache.v4/cache.db differ diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index e9a124831..ca47fa872 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -102,6 +102,124 @@ export interface RequestConfig { withCredentials?: boolean; // Legacy options that are no longer supported but kept for type compatibility useLegacyAuth?: boolean; +} + +/** + * Represents a normalized API error returned by the backend. + */ +export interface ApiError extends Error { + code: number; + message: string; + requestId?: string; + details?: Record; + path?: string; + suggestion?: string; +} + +/** + * Represents a successful API response. + */ +export interface ApiResponse { + data: T; + status: number; + requestId?: string; +} + +/** + * Normalizes fetch responses to either ApiResponse or throws ApiError. + * Handles JSON and text error bodies, preserves request IDs and status codes. + */ +export async function request( + input: RequestInfo, + init?: RequestInit & RequestConfig +): Promise> { + const response = await fetch(input, init); + + const contentType = response.headers.get('content-type') || ''; + const isJson = contentType.includes('application/json'); + + if (response.ok) { + // Successful 2xx response + if (isJson) { + const json = await response.json(); + return { + data: json as T, + status: response.status, + requestId: response.headers.get('X-Request-Id') || undefined, + }; + } else if (init?.responseType === 'text') { + const text = await response.text(); + return { + data: text as unknown as T, + status: response.status, + requestId: response.headers.get('X-Request-Id') || undefined, + }; + } else if (init?.responseType === 'blob') { + const blob = await response.blob(); + return { + data: blob as unknown as T, + status: response.status, + requestId: response.headers.get('X-Request-Id') || undefined, + }; + } else { + // Fallback to text + const text = await response.text(); + return { + data: text as unknown as T, + status: response.status, + requestId: response.headers.get('X-Request-Id') || undefined, + }; + } + } else { + // Non-2xx response: parse error body and throw ApiError + let errorMessage = `HTTP error ${response.status}`; + let errorDetails: Record | undefined; + let requestId = response.headers.get('X-Request-Id') || undefined; + let errorPath: string | undefined; + let errorSuggestion: string | undefined; + + try { + if (isJson) { + const errorJson = await response.json(); + if (typeof errorJson === 'object' && errorJson !== null) { + if (typeof errorJson.message === 'string') { + errorMessage = errorJson.message; + } + if (typeof errorJson.details === 'object' && errorJson.details !== null) { + errorDetails = errorJson.details; + } + if (typeof errorJson.request_id === 'string') { + requestId = errorJson.request_id; + } + if (typeof errorJson.path === 'string') { + errorPath = errorJson.path; + } + if (typeof errorJson.suggestion === 'string') { + errorSuggestion = errorJson.suggestion; + } + } + } else { + // Try to parse as text + const text = await response.text(); + if (text) { + errorMessage = text; + } + } + } catch { + // Ignore parse errors, keep default message + } + + const error: ApiError = new Error(errorMessage) as ApiError; + error.code = response.status; + error.message = errorMessage; + error.requestId = requestId; + error.details = errorDetails; + error.path = errorPath; + error.suggestion = errorSuggestion; + + throw error; + } +} enableRetry?: boolean; transformResponse?: boolean; }