From 1119bf045a78a230481c3808791833fffecf7605 Mon Sep 17 00:00:00 2001 From: DrMelone <27028174+Classic298@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:56:10 +0200 Subject: [PATCH] fix: await processing end-to-end so replace status check is reliable; settle Firefox picker on cancel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - process_uploaded_file now awaits _process_handler (previously the coroutine was created and discarded), and the inner process_file calls inside _process_handler are awaited too. Without these awaits, uploads with process=True / process_in_background=False returned before processing ever ran, so data.status stayed 'pending' forever — which is exactly why upload_and_replace_file's Step 2 check (status == 'completed') was unreliable for valid files. Fix the upstream root cause instead of polling for status downstream. - Firefox directory-picker fallback in collectDirectoryFiles now always settles its Promise: handlers are consolidated through a single finish() helper, the modern 'cancel' event is wired up, and a window-focus + 500ms sentinel resolves with empty files when the picker dismisses without any event. Without this, cancelling the picker in Firefox left the caller stuck on "Scanning directory..." with no completion path. --- backend/open_webui/routers/files.py | 10 +-- .../workspace/Knowledge/KnowledgeBase.svelte | 63 ++++++++++++++----- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 825565e48d..99535cd296 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -115,7 +115,7 @@ async def process_uploaded_file( file_path_processed = Storage.get_file(file_path) result = transcribe(request, file_path_processed, file_metadata, user) - process_file( + await process_file( request, ProcessFileForm(file_id=file_item.id, content=result.get('text', '')), user=user, @@ -124,7 +124,7 @@ async def process_uploaded_file( elif (not content_type.startswith(('image/', 'video/'))) or ( request.app.state.config.CONTENT_EXTRACTION_ENGINE == 'external' ): - process_file( + await process_file( request, ProcessFileForm(file_id=file_item.id), user=user, @@ -134,7 +134,7 @@ async def process_uploaded_file( raise Exception(f'File type {content_type} is not supported for processing') else: log.info(f'File type {file.content_type} is not provided, but trying to process anyway') - process_file( + await process_file( request, ProcessFileForm(file_id=file_item.id), user=user, @@ -153,10 +153,10 @@ async def process_uploaded_file( ) if db: - _process_handler(db) + await _process_handler(db) else: with SessionLocal() as db_session: - _process_handler(db_session) + await _process_handler(db_session) @router.post('/', response_model=FileModelResponse) diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index d1deba076b..f04d0efea1 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -469,45 +469,78 @@ input.directory = true; input.multiple = true; input.style.display = 'none'; - + document.body.appendChild(input); - + + // Cancelling the native file picker does not always fire + // 'change' or 'error' — especially in Firefox, where the + // picker can dismiss without any event, leaving the caller + // stuck on "Scanning directory..." forever. Use 'cancel' + // (modern browsers) plus a window-focus + delayed sentinel + // fallback so the Promise ALWAYS settles: either we got + // files, or we resolve with an empty list and the caller's + // existing empty-check toasts. + let settled = false; + const finish = (err?: unknown) => { + if (settled) return; + settled = true; + if (input.parentNode) { + input.parentNode.removeChild(input); + } + window.removeEventListener('focus', onFocus); + if (err) { + reject(err); + } else { + resolve(); + } + }; + const onFocus = () => { + // 'change' fires after 'focus' returns to the window, so + // wait briefly before deciding the user cancelled. + setTimeout(() => finish(), 500); + }; + input.onchange = async () => { try { const inputFiles = Array.from(input.files || []).filter( (file) => !hasHiddenFolder(file.webkitRelativePath) && !file.name.startsWith('.') ); - + for (const file of inputFiles) { const relativePath = file.webkitRelativePath || file.name; const fileWithPath = new File([file], relativePath, { type: file.type }); - + const collectedFile: CollectedFile = { file: fileWithPath, path: relativePath, size: file.size }; - + if (withHashes) { collectedFile.hash = await calculateFileHash(file); } - + files.push(collectedFile); } - - document.body.removeChild(input); - resolve(); + + finish(); } catch (error) { - document.body.removeChild(input); - reject(error); + finish(error); } }; - + input.onerror = (error) => { - document.body.removeChild(input); - reject(error); + finish(error); }; - + + // Newer browsers fire 'cancel' when the picker is dismissed + // without a selection; cast because older lib.dom typings may + // not yet declare it. + (input as any).oncancel = () => { + finish(); + }; + + window.addEventListener('focus', onFocus, { once: true }); input.click(); }); }