feat: Implement comprehensive OpenAI error handling for Issue #362

Replace silent failures with clear, actionable error messages to eliminate
90-minute debugging sessions when OpenAI API quota is exhausted.

## Backend Enhancements
- Add error sanitization preventing sensitive data exposure (API keys, URLs, tokens)
- Add upfront API key validation before expensive operations (crawl, upload, refresh)
- Implement fail-fast pattern in RAG service (no more empty results for API failures)
- Add specific error handling for quota, rate limit, auth, and API errors
- Add EmbeddingAuthenticationError exception with masked key prefix support

## Frontend Enhancements
- Create enhanced error utilities with OpenAI-specific parsing
- Build TanStack Query compatible API wrapper preserving ETag caching
- Update knowledge service to use enhanced error handling
- Enhance TanStack Query hooks with user-friendly error messages

## Security Features
- Comprehensive regex sanitization (8 patterns) with ReDoS protection
- Input validation and circular reference detection
- Generic fallback messages for sensitive keywords
- Bounded quantifiers to prevent regex DoS attacks

## User Experience
- Clear error messages: "OpenAI API quota exhausted"
- Actionable guidance: "Check your OpenAI billing dashboard and add credits"
- Immediate error visibility (no more silent failures)
- Appropriate error severity styling

## Architecture Compatibility
- Full TanStack Query integration maintained
- ETag caching and optimistic updates preserved
- No performance regression (all existing tests pass)
- Compatible with existing knowledge base architecture

Resolves #362: Users no longer experience mysterious empty RAG results
that require extensive debugging to identify OpenAI quota issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
leex279
2025-09-12 19:22:36 +02:00
parent 94aed6b9fa
commit 98b798173e
26 changed files with 1375 additions and 143 deletions

View File

@@ -0,0 +1,99 @@
/**
* Enhanced API client for knowledge base operations with OpenAI error handling
* Built on top of the ETag-aware API client with additional error parsing
*/
import { callAPIWithETag } from "../../projects/shared/apiWithEtag";
import { parseKnowledgeBaseError, type EnhancedError } from "../utils/errorHandler";
/**
* API call wrapper with enhanced OpenAI error handling
* Uses ETag caching for efficiency while adding specialized error parsing
*/
export async function callKnowledgeAPI<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
try {
// Use the ETag-aware API client for caching benefits
return await callAPIWithETag<T>(endpoint, options);
} catch (error: any) {
// Apply enhanced error parsing for OpenAI errors
const enhancedError = parseKnowledgeBaseError({
status: error.statusCode || error.status,
error: error.message || error.detail || error,
detail: error.detail
});
// Preserve the original error structure but enhance with our parsing
const finalError = error as EnhancedError;
finalError.isOpenAIError = enhancedError.isOpenAIError;
finalError.errorDetails = enhancedError.errorDetails;
finalError.message = enhancedError.message;
throw finalError;
}
}
/**
* Enhanced upload wrapper that handles FormData and file uploads with better error handling
*/
export async function uploadWithEnhancedErrors(
endpoint: string,
formData: FormData,
timeoutMs: number = 30000
): Promise<any> {
const API_BASE_URL = "/api"; // Use same base as other services
let fullUrl = `${API_BASE_URL}${endpoint}`;
// Handle test environment URLs
if (typeof process !== "undefined" && process.env?.NODE_ENV === "test") {
const testHost = process.env?.VITE_HOST || "localhost";
const testPort = process.env?.ARCHON_SERVER_PORT || "8181";
fullUrl = `http://${testHost}:${testPort}${fullUrl}`;
}
try {
const response = await fetch(fullUrl, {
method: "POST",
body: formData,
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch {
const text = await response.text();
errorData = { status: response.status, error: text };
}
// Apply enhanced error parsing
const enhancedError = parseKnowledgeBaseError({
status: response.status,
error: errorData.detail || errorData.error || errorData,
detail: errorData.detail
});
throw enhancedError;
}
return response.json();
} catch (error: any) {
// Check if it's a timeout error
if (error instanceof Error && error.name === 'AbortError') {
const timeoutError = parseKnowledgeBaseError(new Error('Request timed out'));
throw timeoutError;
}
// If it's already an enhanced error, re-throw it
if (error && typeof error === 'object' && 'isOpenAIError' in error) {
throw error;
}
// Parse other errors through the error handler for consistency
throw parseKnowledgeBaseError(error);
}
}