From 2355b8a3699826eaf1fe5a344f63e292f9532c48 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:23:05 +0200 Subject: [PATCH] fix: requests hang or fail with 503 after pages are closed while loading (#32617) Closing a page while the server was still sending it data (a file's processing status, a chat export) could interrupt a database read halfway. On SQLite the connection used for it was then never given back, and a later unrelated request could hang forever when that connection was finally cleaned up; once enough were lost, every request needing the database waited 30 seconds and failed with a 503. The connection is now kept and rolled back instead of being thrown away inside the interrupted request, and the session is closed once instead of twice, as either alone still lost connections. Closing 600 chat exports and 300 file status streams mid-load on SQLite used to lose 60 (the whole pool in that test) and 56 connections, now none; Postgres lost none before or after. Fixes #32609 --- backend/open_webui/internal/db.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/open_webui/internal/db.py b/backend/open_webui/internal/db.py index ed691663fb..7433f1cd5f 100644 --- a/backend/open_webui/internal/db.py +++ b/backend/open_webui/internal/db.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import logging import os import re @@ -476,6 +477,12 @@ if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL: @event.listens_for(async_engine.sync_engine, 'connect') def _set_sqlite_pragmas(dbapi_connection, connection_record): _apply_sqlite_pragmas(dbapi_connection) + + @event.listens_for(async_engine.sync_engine, 'handle_error') + def _keep_connection_on_cancel(context): + # invalidating here would close the connection inside the cancelled task, and that close is lost + if isinstance(context.original_exception, asyncio.CancelledError): + context.is_disconnect = False else: if isinstance(DATABASE_POOL_SIZE, int): if DATABASE_POOL_SIZE > 0: @@ -514,20 +521,14 @@ AsyncSessionLocal = async_sessionmaker( async def get_async_session(): """Async session generator for FastAPI Depends().""" async with AsyncSessionLocal() as db: - try: - yield db - finally: - await db.close() + yield db @asynccontextmanager async def get_async_db(): """Async context manager for use outside of FastAPI dependency injection.""" async with AsyncSessionLocal() as db: - try: - yield db - finally: - await db.close() + yield db @asynccontextmanager