| | |
| | |
| | |
| | |
| |
|
| | 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, |
| | }, |
| | }; |
| | } |
| | } |
| |
|