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