From 69231d0f2c84f38e48f94539ff09da15f9d1d753 Mon Sep 17 00:00:00 2001 From: Drol Date: Mon, 27 Jul 2026 21:27:50 +0800 Subject: [PATCH] fix(ai): improve document loader timeout and PDF errors (#10172) * fix(ai): return clean error message instead of stack trace from document loader worker - Catch PasswordException in loadPdf and throw a user-friendly message - Send error.message instead of error.stack back to main thread * feat(ai): add timeout support to document loader worker - Add configurable timeout option (default 5 minutes) to loadByWorker - Terminate worker and reject with clear error on timeout --- packages/core/ai/src/document-loader/index.ts | 10 ++++++++++ .../core/ai/src/document-loader/loader.worker.ts | 13 +++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/core/ai/src/document-loader/index.ts b/packages/core/ai/src/document-loader/index.ts index eecd2c69aef..3b12a625da2 100644 --- a/packages/core/ai/src/document-loader/index.ts +++ b/packages/core/ai/src/document-loader/index.ts @@ -14,14 +14,19 @@ import path from 'node:path'; export type DocumentLoaderWorkerOptions = { filePath: string; mimeType?: string; + /** Timeout in milliseconds for the worker to complete. Defaults to 5 minutes. */ + timeout?: number; }; +const DEFAULT_WORKER_TIMEOUT = 5 * 60 * 1000; + export const loadByWorker = async (extname: string, options: DocumentLoaderWorkerOptions): Promise => { const isTsRuntime = __filename.endsWith('.ts'); const workerPath = path.join(__dirname, `loader.worker.${isTsRuntime ? 'ts' : 'js'}`); const worker = new Worker(workerPath, { execArgv: isTsRuntime ? ['--require', 'tsx/cjs'] : undefined, }); + const timeout = options.timeout ?? DEFAULT_WORKER_TIMEOUT; return new Promise((resolve, reject) => { let settled = false; const close = (error?: Error, result?: Document[]) => { @@ -29,6 +34,7 @@ export const loadByWorker = async (extname: string, options: DocumentLoaderWorke return; } settled = true; + clearTimeout(timer); if (error) { reject(error); return; @@ -36,6 +42,10 @@ export const loadByWorker = async (extname: string, options: DocumentLoaderWorke resolve(result || []); }; + const timer = setTimeout(() => { + close(new Error(`Document loading timed out after ${Math.round(timeout / 1000)}s`)); + }, timeout); + worker.once('message', (payload: { documents?: Document[]; error?: string }) => { if (payload?.error) { close(new Error(payload.error)); diff --git a/packages/core/ai/src/document-loader/loader.worker.ts b/packages/core/ai/src/document-loader/loader.worker.ts index f81cc8eba87..36a6d752659 100644 --- a/packages/core/ai/src/document-loader/loader.worker.ts +++ b/packages/core/ai/src/document-loader/loader.worker.ts @@ -29,7 +29,15 @@ type WorkerResponse = { const loadPdf = async (filePath: string): Promise => { const loader = new PDFLoader(filePath); - return loader.load(); + try { + return await loader.load(); + } catch (error) { + const err = error as Error; + if (err?.name === 'PasswordException' || /password/i.test(err?.message)) { + throw new Error('The PDF file is password-protected and cannot be parsed. Please upload an unlocked version.'); + } + throw error; + } }; const loadDoc = async (filePath: string, type: 'docx' | 'doc'): Promise => { @@ -89,8 +97,9 @@ parentPort?.on('message', async (payload: ParsePayload) => { }; parentPort?.postMessage(response); } catch (error) { + const err = error as Error; const response: WorkerResponse = { - error: String(error?.stack || error), + error: err?.message || String(error), }; parentPort?.postMessage(response); }