mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
3.0 KiB
132 lines
3.0 KiB
export interface ClassifiedToolError {
|
|
type:
|
|
| 'timeout'
|
|
| 'not_found'
|
|
| 'permission'
|
|
| 'validation'
|
|
| 'upstream'
|
|
| 'unknown';
|
|
userMessage: string;
|
|
retryable: boolean;
|
|
}
|
|
|
|
export function classifyToolError(error: unknown): ClassifiedToolError {
|
|
if (!(error instanceof Error)) {
|
|
return {
|
|
type: 'unknown',
|
|
userMessage: 'An unexpected error occurred while fetching data.',
|
|
retryable: false
|
|
};
|
|
}
|
|
|
|
const message = error.message.toLowerCase();
|
|
|
|
if (
|
|
error.name === 'AbortError' ||
|
|
message.includes('timeout') ||
|
|
message.includes('aborted')
|
|
) {
|
|
return {
|
|
type: 'timeout',
|
|
userMessage: 'The request timed out. The data source may be slow.',
|
|
retryable: true
|
|
};
|
|
}
|
|
|
|
if (message.includes('not found') || message.includes('404')) {
|
|
return {
|
|
type: 'not_found',
|
|
userMessage: 'The requested data was not found.',
|
|
retryable: false
|
|
};
|
|
}
|
|
|
|
if (
|
|
message.includes('unauthorized') ||
|
|
message.includes('forbidden') ||
|
|
message.includes('401') ||
|
|
message.includes('403')
|
|
) {
|
|
return {
|
|
type: 'permission',
|
|
userMessage: 'Access denied to the requested resource.',
|
|
retryable: false
|
|
};
|
|
}
|
|
|
|
if (message.includes('unique constraint') || message.includes('p2002')) {
|
|
return {
|
|
type: 'validation',
|
|
userMessage:
|
|
'A record with this name already exists. Choose a different name.',
|
|
retryable: false
|
|
};
|
|
}
|
|
|
|
if (
|
|
message.includes('invalid') ||
|
|
message.includes('validation') ||
|
|
message.includes('required')
|
|
) {
|
|
return {
|
|
type: 'validation',
|
|
userMessage: `Data unavailable: ${error.message.slice(0, 200)}. Do not retry with different parameters.`,
|
|
retryable: false
|
|
};
|
|
}
|
|
|
|
if (
|
|
message.includes('econnrefused') ||
|
|
message.includes('econnreset') ||
|
|
message.includes('enotfound') ||
|
|
message.includes('upstream') ||
|
|
message.includes('503')
|
|
) {
|
|
return {
|
|
type: 'upstream',
|
|
userMessage: 'An external data source is temporarily unavailable.',
|
|
retryable: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
type: 'unknown',
|
|
userMessage: `An unexpected error occurred: ${error.message.slice(0, 200)}`,
|
|
retryable: false
|
|
};
|
|
}
|
|
|
|
export function withTimeout<T>(
|
|
promise: Promise<T>,
|
|
timeoutMs: number = 10_000
|
|
): Promise<T> {
|
|
let timer: ReturnType<typeof setTimeout>;
|
|
|
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
timer = setTimeout(() => {
|
|
reject(
|
|
Object.assign(new Error('Tool execution timed out'), {
|
|
name: 'AbortError'
|
|
})
|
|
);
|
|
}, timeoutMs);
|
|
// Ensure timer doesn't prevent Node process exit
|
|
if (typeof timer === 'object' && 'unref' in timer) {
|
|
timer.unref();
|
|
}
|
|
});
|
|
|
|
// Clear the timer when the original promise settles first
|
|
const wrappedPromise = promise.then(
|
|
(result) => {
|
|
clearTimeout(timer);
|
|
return result;
|
|
},
|
|
(error) => {
|
|
clearTimeout(timer);
|
|
throw error;
|
|
}
|
|
);
|
|
|
|
return Promise.race([wrappedPromise, timeoutPromise]);
|
|
}
|
|
|