mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-01 04:09:08 -05:00
Clean production-ready version with: - Comprehensive OpenAI error handling (401, 429, 502) - Proper error message sanitization - ProjectServiceError structure handling - Enhanced user-friendly error messages - TanStack Query integration Successfully resolves Issue #362 - users now get immediate clear error messages instead of silent failures and 90-minute debugging sessions.
163 lines
5.4 KiB
TypeScript
163 lines
5.4 KiB
TypeScript
/**
|
|
* 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) {
|
|
// Handle ProjectServiceError specifically (comes from callAPIWithETag)
|
|
let errorData;
|
|
if (error.constructor?.name === 'ProjectServiceError') {
|
|
// The ETag client extracts the error message but loses the structured details
|
|
// We need to reconstruct the structured error based on the status code and message
|
|
|
|
if (error.statusCode === 401 && error.message === "Invalid OpenAI API key") {
|
|
// This is our OpenAI authentication error
|
|
errorData = {
|
|
status: 401,
|
|
error: error.message,
|
|
detail: {
|
|
error: "Invalid OpenAI API key",
|
|
message: "Please verify your OpenAI API key in Settings before starting a crawl.",
|
|
error_type: "authentication_failed"
|
|
}
|
|
};
|
|
} else if (error.statusCode === 429 && error.message === "OpenAI quota exhausted") {
|
|
// This is our OpenAI quota error
|
|
errorData = {
|
|
status: 429,
|
|
error: error.message,
|
|
detail: {
|
|
error: "OpenAI quota exhausted",
|
|
message: "Your OpenAI API key has no remaining credits. Please add credits to your account.",
|
|
error_type: "quota_exhausted"
|
|
}
|
|
};
|
|
} else if (error.statusCode === 429 && error.message === "OpenAI API rate limit exceeded") {
|
|
// This is our rate limit error
|
|
errorData = {
|
|
status: 429,
|
|
error: error.message,
|
|
detail: {
|
|
error: "OpenAI API rate limit exceeded",
|
|
message: "Too many requests to OpenAI API. Please wait a moment and try again.",
|
|
error_type: "rate_limit",
|
|
retry_after: 30
|
|
}
|
|
};
|
|
} else if (error.statusCode === 502 && error.message === "OpenAI API error") {
|
|
// This is our generic API error
|
|
errorData = {
|
|
status: 502,
|
|
error: error.message,
|
|
detail: {
|
|
error: "OpenAI API error",
|
|
message: "OpenAI API error. Please check your API key configuration.",
|
|
error_type: "api_error"
|
|
}
|
|
};
|
|
} else {
|
|
// For other ProjectServiceErrors, use the message as-is
|
|
errorData = {
|
|
status: error.statusCode,
|
|
error: error.message,
|
|
detail: null
|
|
};
|
|
}
|
|
} else {
|
|
// Handle other error types
|
|
errorData = {
|
|
status: error.statusCode || error.status,
|
|
error: error.message || error.detail || error,
|
|
detail: error.detail
|
|
};
|
|
}
|
|
|
|
// Apply enhanced error parsing for OpenAI errors
|
|
const enhancedError = parseKnowledgeBaseError(errorData);
|
|
|
|
// 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);
|
|
}
|
|
} |