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

@@ -11,6 +11,7 @@ import { useActiveOperations } from "../progress/hooks";
import { progressKeys } from "../progress/hooks/useProgressQueries";
import type { ActiveOperation, ActiveOperationsResponse } from "../progress/types";
import { knowledgeService } from "../services";
import { getDisplayErrorMessage, type EnhancedError } from "../utils/errorHandler";
import type {
CrawlRequest,
CrawlStartResponse,
@@ -273,7 +274,10 @@ export function useCrawlUrl() {
queryClient.setQueryData(progressKeys.list(), context.previousOperations);
}
const errorMessage = error instanceof Error ? error.message : "Failed to start crawl";
// Use enhanced error handling for better user experience
const errorMessage = (error as EnhancedError)?.isOpenAIError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to start crawl");
showToast(errorMessage, "error");
},
});
@@ -449,8 +453,10 @@ export function useUploadDocument() {
queryClient.setQueryData(progressKeys.list(), context.previousOperations);
}
// Display the actual error message from backend
const message = error instanceof Error ? error.message : "Failed to upload document";
// Use enhanced error handling for better user experience
const message = (error as EnhancedError)?.isOpenAIError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to upload document");
showToast(message, "error");
},
});
@@ -521,7 +527,10 @@ export function useDeleteKnowledgeItem() {
queryClient.setQueryData(queryKey, data);
}
const errorMessage = error instanceof Error ? error.message : "Failed to delete item";
// Use enhanced error handling for better user experience
const errorMessage = (error as EnhancedError)?.isOpenAIError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to delete item");
showToast(errorMessage, "error");
},
onSuccess: (data) => {
@@ -568,7 +577,10 @@ export function useUpdateKnowledgeItem() {
queryClient.setQueryData(knowledgeKeys.detail(variables.sourceId), context.previousItem);
}
const errorMessage = error instanceof Error ? error.message : "Failed to update item";
// Use enhanced error handling for better user experience
const errorMessage = (error as EnhancedError)?.isOpenAIError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to update item");
showToast(errorMessage, "error");
},
onSuccess: (_data, { sourceId }) => {
@@ -604,7 +616,10 @@ export function useRefreshKnowledgeItem() {
return data;
},
onError: (error) => {
const errorMessage = error instanceof Error ? error.message : "Failed to refresh item";
// Use enhanced error handling for better user experience
const errorMessage = (error as EnhancedError)?.isOpenAIError
? getDisplayErrorMessage(error as EnhancedError)
: (error instanceof Error ? error.message : "Failed to refresh item");
showToast(errorMessage, "error");
},
});