| |
| |
| |
| |
|
|
| export type ErrorCategory = |
| | "retryable" |
| | "non_retryable" |
| | "rate_limit" |
| | "timeout" |
| | "network" |
| | "validation" |
| | "not_found" |
| | "unauthorized" |
| | "ai_content_blocked" |
| | "ai_quota" |
| | "unsupported_file_type"; |
|
|
| export interface ClassifiedError { |
| category: ErrorCategory; |
| retryable: boolean; |
| retryDelay?: number; |
| maxRetries?: number; |
| } |
|
|
| |
| |
| |
| |
| export class NonRetryableError extends Error { |
| constructor( |
| message: string, |
| public readonly originalError?: unknown, |
| public readonly category: ErrorCategory = "non_retryable", |
| ) { |
| super(message); |
| this.name = "NonRetryableError"; |
| |
| if (Error.captureStackTrace) { |
| Error.captureStackTrace(this, NonRetryableError); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| export class UnsupportedFileTypeError extends NonRetryableError { |
| constructor( |
| public readonly mimetype: string, |
| public readonly fileName: string, |
| ) { |
| super( |
| `File type ${mimetype} is not supported for processing`, |
| undefined, |
| "unsupported_file_type", |
| ); |
| this.name = "UnsupportedFileTypeError"; |
| } |
| } |
|
|
| |
| |
| |
| export function classifyError(error: unknown): ClassifiedError { |
| |
| if (error instanceof Error && error.name === "TimeoutError") { |
| return { |
| category: "timeout", |
| retryable: true, |
| retryDelay: 2000, |
| maxRetries: 3, |
| }; |
| } |
|
|
| |
| if (error instanceof Error && error.name === "AbortError") { |
| return { |
| category: "timeout", |
| retryable: true, |
| retryDelay: 2000, |
| maxRetries: 3, |
| }; |
| } |
|
|
| |
| if (error instanceof Error) { |
| const message = error.message.toLowerCase(); |
| const stack = error.stack?.toLowerCase() || ""; |
|
|
| |
| if ( |
| message.includes("network") || |
| message.includes("econnreset") || |
| message.includes("enotfound") || |
| message.includes("econnrefused") || |
| message.includes("etimedout") || |
| stack.includes("fetch") |
| ) { |
| return { |
| category: "network", |
| retryable: true, |
| retryDelay: 1000, |
| maxRetries: 3, |
| }; |
| } |
|
|
| |
| |
| if ( |
| message.includes("content filtered") || |
| message.includes("content_filter") || |
| message.includes("safety") || |
| message.includes("blocked") || |
| message.includes("harm_category") || |
| message.includes("finish_reason") || |
| message.includes("recitation") |
| ) { |
| return { |
| category: "ai_content_blocked", |
| retryable: false, |
| }; |
| } |
|
|
| |
| |
| if ( |
| message.includes("quota exceeded") || |
| message.includes("resource_exhausted") || |
| message.includes("overloaded") || |
| message.includes("model_overloaded") || |
| message.includes("capacity") |
| ) { |
| return { |
| category: "ai_quota", |
| retryable: true, |
| retryDelay: 60_000, |
| maxRetries: 3, |
| }; |
| } |
|
|
| |
| if ( |
| message.includes("rate limit") || |
| message.includes("429") || |
| message.includes("too many requests") || |
| message.includes("quota") |
| ) { |
| return { |
| category: "rate_limit", |
| retryable: true, |
| retryDelay: 5000, |
| maxRetries: 5, |
| }; |
| } |
|
|
| |
| |
| if ( |
| (message.includes("download") || message.includes("downloaderror")) && |
| (message.includes("400") || message.includes("bad request")) && |
| (message.includes("token") || |
| message.includes("sign") || |
| message.includes("signed")) |
| ) { |
| return { |
| category: "network", |
| retryable: true, |
| retryDelay: 1000, |
| maxRetries: 3, |
| }; |
| } |
|
|
| |
| if ( |
| message.includes("validation") || |
| message.includes("invalid") || |
| message.includes("malformed") || |
| message.includes("bad request") || |
| message.includes("400") |
| ) { |
| return { |
| category: "validation", |
| retryable: false, |
| }; |
| } |
|
|
| |
| if ( |
| message.includes("not found") || |
| message.includes("404") || |
| message.includes("does not exist") |
| ) { |
| return { |
| category: "not_found", |
| retryable: false, |
| }; |
| } |
|
|
| |
| if ( |
| message.includes("unauthorized") || |
| message.includes("401") || |
| message.includes("forbidden") || |
| message.includes("403") || |
| message.includes("authentication") || |
| message.includes("permission") |
| ) { |
| return { |
| category: "unauthorized", |
| retryable: false, |
| }; |
| } |
|
|
| |
| if ( |
| message.includes("500") || |
| message.includes("502") || |
| message.includes("503") || |
| message.includes("504") || |
| message.includes("internal server error") || |
| message.includes("service unavailable") || |
| message.includes("bad gateway") || |
| message.includes("gateway timeout") |
| ) { |
| return { |
| category: "retryable", |
| retryable: true, |
| retryDelay: 2000, |
| maxRetries: 3, |
| }; |
| } |
| } |
|
|
| |
| return { |
| category: "retryable", |
| retryable: true, |
| retryDelay: 1000, |
| maxRetries: 3, |
| }; |
| } |
|
|
| |
| |
| |
| export function isRetryableError(error: unknown): boolean { |
| return classifyError(error).retryable; |
| } |
|
|
| |
| |
| |
| export function getRetryDelay(error: unknown): number { |
| const classified = classifyError(error); |
| return classified.retryDelay || 1000; |
| } |
|
|
| |
| |
| |
| export function getMaxRetries(error: unknown): number { |
| const classified = classifyError(error); |
| return classified.maxRetries ?? 3; |
| } |
|
|
| |
| |
| |
| export function isNonRetryableError(error: unknown): boolean { |
| return error instanceof NonRetryableError; |
| } |
|
|
| |
| |
| |
| |
| export function getJobRetryOptions(errorCategory?: ErrorCategory): { |
| attempts: number; |
| backoff: { |
| type: "exponential" | "fixed"; |
| delay: number; |
| }; |
| removeOnFail: boolean | { age: number; count?: number }; |
| } { |
| switch (errorCategory) { |
| case "rate_limit": |
| return { |
| attempts: 5, |
| backoff: { |
| type: "exponential", |
| delay: 5000, |
| }, |
| removeOnFail: { |
| age: 7 * 24 * 3600, |
| }, |
| }; |
| case "timeout": |
| case "network": |
| return { |
| attempts: 3, |
| backoff: { |
| type: "exponential", |
| delay: 2000, |
| }, |
| removeOnFail: { |
| age: 7 * 24 * 3600, |
| }, |
| }; |
| case "validation": |
| case "not_found": |
| case "unauthorized": |
| case "ai_content_blocked": |
| return { |
| attempts: 1, |
| backoff: { |
| type: "fixed", |
| delay: 0, |
| }, |
| removeOnFail: { |
| age: 24 * 3600, |
| }, |
| }; |
| case "ai_quota": |
| return { |
| attempts: 3, |
| backoff: { |
| type: "exponential", |
| delay: 60000, |
| }, |
| removeOnFail: { |
| age: 7 * 24 * 3600, |
| }, |
| }; |
| default: |
| return { |
| attempts: 3, |
| backoff: { |
| type: "exponential", |
| delay: 1000, |
| }, |
| removeOnFail: { |
| age: 7 * 24 * 3600, |
| }, |
| }; |
| } |
| } |
|
|