From f165f7dc3d4768ea5ad60617bed6f271080e9c49 Mon Sep 17 00:00:00 2001 From: DrMelone <27028174+Classic298@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:22:58 +0200 Subject: [PATCH] fix: offload blocking storage and file reads to worker thread Storage.get_file and local file reads can perform sync network/disk I/O for remote backends (S3/GCS/Azure), stalling the event loop during chat-triggered uploads. Run them via asyncio.to_thread and pass bytes to the FormData field instead of a sync file handle. --- backend/open_webui/tools/builtin.py | 48 +++++++++++++++++------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index d7e1ec9c84..b355eb12d9 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -2830,6 +2830,11 @@ async def delete_automation( # ============================================================================= +def _read_file_bytes(path: str) -> bytes: + with open(path, 'rb') as fh: + return fh.read() + + async def upload_file_to_terminal( file_id: str, __request__: Request = None, @@ -2922,7 +2927,11 @@ async def upload_file_to_terminal( ): return json.dumps({'error': 'File not found'}) - local_path = Storage.get_file(file_record.path) + # Storage.get_file and file reads can do sync network/disk I/O for + # remote backends (S3/GCS/Azure); offload to a worker thread to keep + # the event loop responsive under concurrent chat load. + local_path = await asyncio.to_thread(Storage.get_file, file_record.path) + file_bytes = await asyncio.to_thread(_read_file_bytes, local_path) # --- 3. Upload to terminal server --- import aiohttp @@ -2950,25 +2959,24 @@ async def upload_file_to_terminal( f'to {terminal_url}/files/upload?directory={upload_dir}' ) - with open(local_path, 'rb') as fh: - form_data = aiohttp.FormData() - form_data.add_field('file', fh, filename=file_record.filename) - async with session.post( - f'{terminal_url}/files/upload', - params={'directory': upload_dir}, - data=form_data, - headers=headers, - cookies=cookies, - ) as response: - if not (200 <= response.status < 300): - detail = (await response.text())[:500] - return json.dumps( - {'error': f'Upload failed (HTTP {response.status}): {detail}'} - ) - try: - result = await response.json() - except Exception: - result = {} + form_data = aiohttp.FormData() + form_data.add_field('file', file_bytes, filename=file_record.filename) + async with session.post( + f'{terminal_url}/files/upload', + params={'directory': upload_dir}, + data=form_data, + headers=headers, + cookies=cookies, + ) as response: + if not (200 <= response.status < 300): + detail = (await response.text())[:500] + return json.dumps( + {'error': f'Upload failed (HTTP {response.status}): {detail}'} + ) + try: + result = await response.json() + except Exception: + result = {} return json.dumps( {