* refac(routers): reject external URLs in profile/model image handlers
* refac(ui): centralize image URL validation in safeImageUrl helper
* refac(auths): make signout POST-only
* refac: gate external profile image redirect behind ENABLE_PROFILE_IMAGE_URL_FORWARDING
Restore the 302 redirect for external http(s) profile image URLs in
the user and model profile-image endpoints, but gate it behind a new
ENABLE_PROFILE_IMAGE_URL_FORWARDING env flag (default: True).
Existing deployments that rely on external profile image forwarding
continue to work unchanged. Operators who want to suppress the
redirect (to prevent client-side IP/UA/Referer leaks) can set the
flag to False.
Non-admin GET /api/v1/prompts/tags went through get_prompts_by_user_id,
which loaded every active prompt with its full content/data/meta plus
owner records and all access grants, then ran one has_access query per
prompt that wasn't owned by the caller - all so the endpoint could
collapse the result to a sorted tag list. With 600 prompts this took
several seconds while the admin path (a single SELECT) returned in <1s.
Add Prompts.get_tags_by_user_id which selects only the tags column and
applies the same EXISTS-based access filter used by /list. Also tighten
the admin get_tags to project just the tags column instead of full rows.
The endpoint is now one DB query (plus one for groups), no row hydration,
no N+1.
Co-authored-by: Claude <noreply@anthropic.com>
Convert the /chats/all endpoint from loading all user chats into memory
at once to a streaming NDJSON response that fetches chats in batches of
100. This prevents Out-of-Memory crashes for users with large chat
histories.
Backend: Added async generator that paginates through chats with
short-lived DB sessions per batch (critical for SQLite lock release).
Frontend: Updated getAllChats to consume the NDJSON stream via
ReadableStream reader, accumulating results for the export file.
Ref: open-webui#22206
The transcription endpoint was async but called the synchronous transcribe() function directly, blocking the single-threaded uvicorn event loop for the entire duration of inference. This caused all HTTP and WebSocket connections to stall for every user on the instance during STT processing.
- Add asyncio import
- Use async UploadFile.read() instead of synchronous file.file.read()
- Offload the blocking transcribe() call via asyncio.to_thread()
Closes#24169
- Return 302 to /static/favicon.png instead of streaming the same PNG per
model id so browsers can cache one asset for default avatars.
- Validate stored /static/ paths with decode, normpath, and /static
prefix checks; invalid paths fall back to favicon.
Made-with: Cursor
* perf(channels): batch user lookup in model_response_handler thread history
The thread-history builder in model_response_handler called
Users.get_user_by_id once per thread message (deduped via an intra-loop
dict), producing N individual SELECTs for a thread of N unique authors.
Replace with a single Users.get_users_by_user_ids call that returns all
authors in one WHERE id IN (...) query, matching the batch pattern
already used elsewhere in this file (lines 739, 804, 1320).
Behavior is preserved: deleted users still resolve to None and fall
through to the existing 'Unknown' fallback via .get().
* refac(channels): rename loop vars to full words per review
Address reviewer feedback to use descriptive names `message` and `user`
instead of single-letter `m` and `u` in the batch user-lookup
comprehensions.
---------
Co-authored-by: Claude <noreply@anthropic.com>
After set_access_grants, the handler was reloading the same knowledge
record via get_knowledge_by_id, which triggers an extra SELECT plus a
nested fetch of access grants. set_access_grants already returns the
newly-written grants and the local knowledge object is otherwise
unchanged, so update it in place and reuse it for the response.
https://claude.ai/code/session_01S18Lgqbih7Ry2JZUUv8TxF
Co-authored-by: Claude <noreply@anthropic.com>