rmccorkl_TubeSage/src/utils/error-utils.ts
Richard McCorkle 1b043d0723 fix: improve timeout error message and detect Gemini DEADLINE_EXCEEDED
Root cause: for 2hr+ transcripts, the Gemini API returns a server-side
DEADLINE_EXCEEDED (504) — not a client-side timeout. The error was being
categorised as a generic timeout and the message told the user to "try again",
which is misleading since retrying the same large transcript will fail again.

Changes:
- error-utils: catch DEADLINE_EXCEEDED/deadline exceeded as Timeout category
- error-utils: replace "try again" timeout message with actionable advice
  (try shorter video, reduce max tokens, or switch model)
- gemini-client: log full error body for easier debugging; surface gRPC
  status code in error message; throw DEADLINE_EXCEEDED prefix on 504 so
  upstream categorisation is unambiguous

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 13:37:09 +02:00

151 lines
5.1 KiB
TypeScript

/**
* Utilities for standardized error handling across the plugin
*/
/**
* Safely extracts error message from any type of error object
* @param error The error object
* @param defaultMessage Default message if extraction fails
* @returns A safe error message string
*/
export function getSafeErrorMessage(error: unknown, defaultMessage = 'Unknown error occurred'): string {
if (error instanceof Error && error.message) {
return error.message;
}
try {
return String(error) || defaultMessage;
} catch {
return defaultMessage;
}
}
/**
* Categories of common errors
*/
enum ErrorCategory {
Network = 'network',
ApiKey = 'api_key',
RateLimit = 'rate_limit',
TokenLimit = 'token_limit',
CORS = 'cors',
Timeout = 'timeout',
NotFound = 'not_found',
Unknown = 'unknown'
}
/**
* Detects error category based on error message content
* @param error The error object
* @returns The detected error category
*/
function detectErrorCategory(error: unknown): ErrorCategory {
const message = getSafeErrorMessage(error);
// Network errors
if (message.includes('network') ||
message.includes('fetch') ||
message.includes('connect') ||
message.includes('ECONNREFUSED') ||
message.includes('NetworkError') ||
message.includes('Failed to fetch')) {
return ErrorCategory.Network;
}
// CORS errors
if (message.includes('CORS') ||
message.includes('Cross-Origin') ||
message.includes('Access-Control-Allow-Origin')) {
return ErrorCategory.CORS;
}
// API key errors
if (message.includes('API key') ||
message.includes('authentication') ||
message.includes('auth') ||
message.includes('unauthorized')) {
return ErrorCategory.ApiKey;
}
// Rate limit errors
if (message.includes('rate limit') ||
message.includes('quota') ||
message.includes('too many requests')) {
return ErrorCategory.RateLimit;
}
// Token limit errors
if (message.includes('context length') ||
message.includes('token limit') ||
message.includes('max_tokens')) {
return ErrorCategory.TokenLimit;
}
// Timeout errors — distinguish server-side deadline (transcript too large) from transient timeouts
if (message.includes('timeout') ||
message.includes('timed out') ||
message.includes('DEADLINE_EXCEEDED') ||
message.includes('deadline exceeded')) {
return ErrorCategory.Timeout;
}
// Not found errors
if (message.includes('not found') ||
message.includes('404')) {
return ErrorCategory.NotFound;
}
return ErrorCategory.Unknown;
}
/**
* Creates a friendly error message for a specific API
* @param error The original error
* @param apiName Name of the API to prefix error with
* @returns Formatted error with appropriate message
*/
function createApiError(error: unknown, apiName: string): Error {
const category = detectErrorCategory(error);
const originalMessage = getSafeErrorMessage(error);
switch (category) {
case ErrorCategory.Network:
return new Error(`[${apiName}] Network error while connecting to service. Please check your internet connection.`);
case ErrorCategory.CORS:
return new Error(`[${apiName}] CORS policy blocked the request. Please check your connection or try a different request.`);
case ErrorCategory.ApiKey:
return new Error(`[${apiName}] Invalid API key or authentication error. Please check your settings.`);
case ErrorCategory.RateLimit:
return new Error(`[${apiName}] Rate limit reached or quota exceeded. Please try again later.`);
case ErrorCategory.TokenLimit:
return new Error(`[${apiName}] Input too long for model's context window.`);
case ErrorCategory.Timeout:
return new Error(`[${apiName}] The request timed out — the transcript is likely too large for the model to process in one pass. Try a shorter video, reduce max tokens, or switch to a model with a longer generation timeout.`);
case ErrorCategory.NotFound:
return new Error(`[${apiName}] Resource not found. Please check the request parameters.`);
default:
// For unknown errors, append the original message
return new Error(`[${apiName}] ${originalMessage}`);
}
}
/**
* Logs an error with standardized formatting and returns a user-friendly error
* @param error The original error
* @param apiName Name of the API to prefix error with
* @param context Additional context for debugging
* @returns Formatted error with appropriate message
*/
export function handleApiError(error: unknown, apiName: string, context?: string): Error {
// Log detailed error for debugging
console.error(`[${apiName}]${context ? ' [' + context + ']' : ''} Error:`, error);
// Return a user-friendly error
return createApiError(error, apiName);
}