Add configurable reranker batch size (env var RAG_RERANKING_BATCH_SIZE,
default 32) following the same pattern as RAG_EMBEDDING_BATCH_SIZE.
- config.py: PersistentConfig for RAG_RERANKING_BATCH_SIZE
- main.py: import, state init, pass to get_reranking_function
- colbert.py: accept batch_size param in predict() (was hardcoded 32)
- utils.py: get_reranking_function passes batch_size at call time
- retrieval.py: expose in config GET/POST endpoints and ConfigForm
- Documents.svelte: add Reranking Batch Size input in admin settings
Closes#23730
* fix(middleware): replace BaseHTTPMiddleware HTTP middlewares with pure ASGI implementations
Starlette's BaseHTTPMiddleware (and the @app.middleware('http')
decorator that uses it) wraps the downstream app in an anyio task
group whose cancel scope tears down the inner task on every exit —
client disconnect, response complete, or any outer middleware bailing.
That CancelledError gets injected into whatever the inner task was
awaiting, so DB queries, embedding calls, and other long awaits get
killed mid-flight. Under aiosqlite the cleanup path then logs a
multi-page `terminate_force_close() not implemented` traceback at
ERROR for every cancelled DB call.
Open WebUI had four such middlewares stacked
(`commit_session_after_request`, `check_url`, `inspect_websocket`,
`RedirectMiddleware`) so a single cancellation would compound through
all four.
Move the four middlewares to a new `open_webui.utils.asgi_middleware`
module as plain ASGI classes (`__call__(scope, receive, send)`):
* `CommitSessionMiddleware` — was `commit_session_after_request`;
now also rolls back if commit fails
before releasing the connection.
* `AuthTokenMiddleware` — was `check_url`; sets request.state
token + enable_api_keys + stamps
X-Process-Time via a wrapped send.
* `WebsocketUpgradeGuardMiddleware`
— was `inspect_websocket`; rejects
/ws/socket.io HTTP requests that
claim transport=websocket without a
proper Upgrade/Connection header.
* `RedirectMiddleware` — was the BaseHTTPMiddleware subclass;
same /watch + share-target rewrites.
Pure ASGI does not introduce a cancel scope around the downstream app,
so client disconnects propagate via `receive()` (the way ASGI was
designed) instead of being injected as CancelledError. Middleware
ordering is preserved.
https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8
* fix(middleware): CommitSessionMiddleware — rollback on downstream error, never commit failed requests
The first cut put commit() in a finally block, which meant that even
when a downstream handler raised, the middleware would still commit
whatever partial sync writes that handler had made before the
failure. That regressed the previous BaseHTTPMiddleware semantics
where commit only ran on the success path.
Restructure the failure handling:
* Downstream raised → rollback any pending sync work, release the
connection, re-raise so the outer error middleware turns it into
an error response. We never commit a request that did not complete.
* Downstream returned → commit. On commit failure, log loudly,
rollback, and re-raise. ScopedSession.remove() always runs in
finally so the connection cannot leak.
Document the inherent pure-ASGI limitation explicitly: by the time
`await self.app(...)` returns the response messages have already
been emitted, so a commit failure can no longer change what the
client sees on the wire. Buffering the response to gate it on commit
success would break streaming responses (chat completions, SSE) which
are core to Open WebUI; the trade-off is intentional. Routes that
need commit-before-send must manage the sync session explicitly.
Also drop unused `typing` imports flagged by review.
https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add ownership checks to global task endpoints
- Restrict GET /api/tasks and POST /api/tasks/stop/{task_id} to admin-only
- Add new scoped POST /api/tasks/chat/{chat_id}/stop endpoint with ownership
check so regular users can stop their own chat tasks
- Allow admins to access the scoped chat task endpoints alongside owners
- Update frontend to use the new scoped stop endpoint when a chatId is available
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
* Handle temporary (local:) chat IDs in scoped task endpoints
Temporary chats use local:<socketId> as chat_id which doesn't exist in
the DB. The scoped endpoints now skip ownership checks for local: IDs
(they aren't enumerable) and use {chat_id:path} to handle the colon in
the URL path.
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
* Verify session ownership for local: chat IDs and URL-encode chat_id
- For local:<socketId> chat IDs, look up the socket's owner in
SESSION_POOL and verify it matches the requesting user (or admin)
- URL-encode chat_id in frontend fetch calls to handle special
characters (colon in local: IDs) safely
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
---------
Co-authored-by: Claude <noreply@anthropic.com>
The APIKeyRestrictionMiddleware only inspected the Authorization header for sk- tokens, but get_current_user also reads API keys from cookies and x-api-key headers. This allowed complete bypass of endpoint restrictions by sending the key via an alternate transport.
Moves the restriction check into get_current_user_by_api_key so it runs regardless of how the API key was delivered. Removes the now-redundant middleware.
Add AUDIT_INCLUDED_PATHS env var for whitelist-based audit filtering.
When set, only matching paths are audited and AUDIT_EXCLUDED_PATHS is
ignored. Auth endpoints (signin/signout/signup) are always logged
regardless of filtering mode.
* fix: replace bare except with except Exception in main.py
* fix: replace bare except with Exception in oauth.py
In Python 3, bare 'except:' is discouraged as it catches all
SystemExit and KeyboardInterrupt exceptions. Changed to 'except Exception:'
to only catch actual exceptions.
* feat: add LOG_FORMAT env var with JSON formatter for early logging
Introduce LOG_FORMAT environment variable (set to "json" to enable).
When active, logging.basicConfig() uses a JSONFormatter that outputs
single-line JSON objects with fields: ts, level, msg, caller, error,
stacktrace. This covers all log messages emitted during module imports
before Loguru's start_logger() takes over.
* feat: add JSON sink for Loguru when LOG_FORMAT=json
Add _json_sink() as a Loguru sink function that writes single-line JSON
to stdout. In start_logger(), conditionally use the JSON sink instead of
the plain-text stdout_format when LOG_FORMAT is set to "json".
* feat: suppress ASCII banner and fix alembic logging in JSON mode
- Wrap the ASCII art banner print in main.py with a LOG_FORMAT != "json"
guard so JSON output stays machine-parseable.
- Skip alembic's fileConfig() call in migrations/env.py when
LOG_FORMAT=json to prevent it from replacing the JSON log handlers
installed during early startup.
fix: model fallback routing for all model types and default model selection
Backend: When ENABLE_CUSTOM_MODEL_FALLBACK is active and a custom model's
base model is unavailable, the fallback now swaps the model and form data
to the configured default model directly. This ensures routing uses the
fallback model's type (pipe, Ollama, or OpenAI) instead of the original
model's type, which previously caused "Model not found" errors when the
fallback was a different backend type.
Frontend: Fixed default model selection in new chat initialization where
the admin-configured default models were always overwritten by the first
available model. The first-available fallback now only triggers when the
configured defaults don't resolve to valid available models.
* perf: eliminate 2 redundant full chat deserialization on every message send (#162)
Problem:
Every message send triggered get_chat_by_id_and_user_id which loads the
entire Chat row — including the potentially massive JSON blob containing
the full conversation history — even when the caller only needed a
simple yes/no ownership check or a single column value.
Two call sites in the message-send hot path were doing this:
1. main.py ownership verification: loaded the entire chat object including
all message history JSON, then checked `if chat is None`. The JSON blob
was immediately discarded — only the existence of the row mattered.
2. middleware.py folder check: loaded the entire chat object including all
message history JSON, then read only `chat.folder_id` — a plain column
on the chat table that requires zero JSON parsing.
Fix:
- Added `chat_exists_by_id_and_user_id()`: uses SQL EXISTS subquery which
returns a boolean without loading any row data. The database can satisfy
this from the primary key index alone.
- Added `get_chat_folder_id()`: queries only the `folder_id` column via
`db.query(Chat.folder_id)`, which tells SQLAlchemy to SELECT only that
single column instead of the entire row.
Both new methods preserve the same error handling semantics (return
False/None on exception) and user_id filtering (ownership check) as
the original get_chat_by_id_and_user_id.
Impact:
- Best case (typical): eliminates deserializing 2 full chat JSON blobs per
message send. For long conversations (hundreds of messages with tool
calls, images, file attachments), this blob can be multiple megabytes.
- Worst case: no regression — the new queries are strictly cheaper than
the old ones (less data transferred, less Python object construction,
no Pydantic model_validate overhead).
- The 3 remaining full chat loads in process_chat_payload (load_messages_from_db,
add_file_context, chat_image_generation_handler) are left untouched as
they genuinely need the full history and require separate analysis.
* Address maintainer feedback: rename method and inline call (#166)
- Rename chat_exists_by_id_and_user_id -> is_chat_owner
- Remove intermediate chat_owned variable; call is_chat_owner directly in if condition