305 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek 5c62cc0517 chore: format 2026-08-25 16:53:53 -04:00
Timothy Jaeryang Baek 170ad0595d refac 2026-08-24 19:48:49 -04:00
Classic298 945c521ed2 refac: make the web search error message a plain constant (#28948)
The web search error message was a lambda with a passthrough branch that returned whatever it was handed. Since #28942 both call sites pass no arguments, so that branch is unreachable, and it is the trap that let a caller drop a raw exception object into an HTTP response body and turn an intended 400 into an unserialisable 500.

A plain string constant removes the trap and lines the message up with every other fixed message in that file. Behaviour is unchanged: the response detail comes out byte for byte identical, because the enum already overrides __str__ to render members as their value. Verified on Python 3.11 and 3.12, both producing the same string and the same JSON body.
2026-08-23 13:27:02 -04:00
Classic298 fca3be5416 fix: web search failures return HTTP 500 with an empty body instead of 400 (#28942)
Any failure during a web search comes back to the client as a bare HTTP 500 with nothing in it. The handler tries to build a 400 whose detail is the caught exception object itself, FastAPI cannot serialise that into a response body, so rendering the error response fails and the request falls through to the generic 500 handler. In chat this surfaces as a web search that fails with no explanation at all, and the most common trigger is simply selecting a search engine without configuring its API key.

This routes the failure through the standard error formatter, which is what the sibling handler for content loading failures in the same function already does. Web search failures now return 400 with a readable message, and the exception itself keeps going to the server log exactly as before.

Passing str(e) into the response was the other option and was rejected: the rest of the backend deliberately keeps provider exception text out of client responses and in the log, and provider exceptions here can carry request details that should not be echoed back.
2026-08-23 12:59:25 -04:00
Timothy Jaeryang Baek 87d9b7e84e refac 2026-08-17 00:51:04 -07:00
Timothy Jaeryang Baek 1a376ac17f refac 2026-08-16 23:21:00 -07:00
Timothy Jaeryang Baek 9c21d4ed3b refac 2026-08-11 17:42:25 -06:00
Timothy Jaeryang Baek 89922cc9d5 refac 2026-08-10 22:53:37 -06:00
Timothy Jaeryang Baek 2a6e671f54 refac 2026-08-10 22:47:39 -06:00
G30 121f2404ee fix(retrieval): report why a URL could not be read instead of blaming the knowledge base (#28362)
Fetching a URL and saving it were reported as one thing. Everything from
reading the URL to writing the vector database sat inside a single try,
whose handler blamed the knowledge base, so a page that could not be
fetched, parsed or resolved was reported as a knowledge base error even
though nothing had reached the knowledge base yet. Reading the URL now has
its own handler that names the URL, and the knowledge base message is left
to the step that actually touches it.

When YouTube refused a transcript the reason was discarded earlier still:
the loader caught the error, logged it, and returned an empty document
list, so the empty result failed downstream and even the salvageable
explanation was gone before a message was produced. The loader now raises
YoutubeTranscriptError carrying a readable reason, mapped from the
transcript library's own exception types. Blocked requests mention that a
proxy can be configured, and disabled, age restricted, unavailable and
missing language cases each say what actually happened.

URLs that attach successfully are unaffected.
2026-08-10 19:18:04 -06:00
Timothy Jaeryang Baek 8fbfd14a8b refac 2026-08-10 01:38:32 -06:00
Classic298 2d18727ab8 perf: build info log messages lazily so raising the log level actually saves work (#27837)
Raising GLOBAL_LOG_LEVEL to WARNING buys quieter output but not less work: 241 INFO call sites interpolate their payload into an f-string before the logging call gets to drop it. The heaviest is get_doc, which logs every chunk id and metadata dict in a collection, so on the full-context retrieval path that is the entire knowledge base, once per chat request.

That one line at WARNING, CPython 3.12:

| knowledge base | payload | before   | after   |
| -------------- | ------- | -------- | ------- |
| top-k of 3     | 1.2 kB  | 3.8 us   | 0.07 us |
| 500 chunks     | 201 kB  | 583.6 us | 0.08 us |
| 5000 chunks    | 2.0 MB  | 5.8 ms   | 0.15 us |

The lazy form log.info('query_doc:result %s %s', result.ids, result.metadatas) hands the payload to record.getMessage(), which the InterceptHandler only reaches once a record has passed the level check. Output at INFO is byte-identical. Two sites that already built their message eagerly, one str concat and one % operator, move to the same lazy form.
2026-08-02 15:39:10 -05:00
Classic298 52cfb02c72 perf: build debug log messages lazily so disabled debug logs cost nothing (#27834)
GLOBAL_LOG_LEVEL defaults to INFO, so every log.debug(...) in the backend is discarded, but the message is built first: 187 call sites interpolate their payload into an f-string before the logging call runs, so the work happens on every request and the result is thrown away. The worst one sits in process_chat_payload and stringifies the whole request body, full conversation history included, once per chat completion.

That one line with DEBUG disabled, CPython 3.12:

| conversation | payload | before   | after   |
| ------------ | ------- | -------- | ------- |
| 4 messages   | 1.2 kB  | 3.4 us   | 0.07 us |
| 20 messages  | 17 kB   | 24.8 us  | 0.07 us |
| 60 messages  | 123 kB  | 216.6 us | 0.07 us |

The lazy form log.debug('form_data: %s', form_data) hands the payload to record.getMessage(), which the InterceptHandler only reaches once a record has passed the level check. With DEBUG enabled the emitted lines are byte-identical, f'{x=}' sites included: those map to %r. MistralLoader._debug_log callers get the same treatment, since that wrapper already forwards *args.
2026-07-31 19:09:01 -05:00
Classic298 6be11d4fc9 chore: remove dead json imports (#27815)
Fourteen modules import `json` without using it. Ruff flags every one with F401, and a word-boundary search for `json` in each file matches only the import line itself, including inside strings, comments and annotations.

Two exclusions, both deliberate. Migration files are left alone: the import is equally dead there, but those files are frozen history and not worth the churn. `models/chats.py` has the same dead import and is handled in its own change, so it is skipped here to avoid two changes touching the same line.

No behaviour change.
2026-07-31 17:25:40 -04:00
Timothy Jaeryang Baek b7394c63fa refac 2026-07-27 03:53:54 -04:00
Timothy Jaeryang Baek db2d24896b refac 2026-07-27 03:32:21 -04:00
Classic298 6d4c02a89e refac: owner-bind ephemeral web-search RAG collections (#26706)
The web-search-* namespace was the one collection namespace filter_accessible_collections admitted unconditionally for any non-admin user, on both read and write, unlike file-*, user-memory-* and knowledge bases which are owner-scoped. process_web_search now mints these ephemeral per-query collections as web-search-{user.id}-<hash>, and the access helper only admits web-search-{requester.id}-* names, so a web-search collection is readable and writable only by the user who created it (admins keep their bypass). The collections hold transient public web-search results and their names are non-enumerable query hashes, so there was no demonstrated cross-user access path; this removes the namespace exception so the per-user scoping the other namespaces enforce also covers web-search.

Co-authored-by: rexpository <rexpository@users.noreply.github.com>
2026-07-27 02:08:04 -04:00
Classic298 6c7478c1c9 fix: surface web search embedding failures in the chat UI instead of silently returning an empty collection (#26883)
Previously, when web search retrieved pages successfully but saving them to the vector DB failed (for example an unreachable or misconfigured embedding endpoint), process_web_search swallowed the exception at debug log level and still returned status: True with the collection name. The chat then showed "Searched N sites" followed by "No sources found" at retrieval time, hiding the actual misconfiguration from the user and making the failure look like a search bug.

process_web_search now logs the failure at exception level and raises an HTTPException with an actionable message pointing at the embedding configuration in Admin Settings > Documents. chat_web_search_handler surfaces the detail of any HTTPException raised during the search in the emitted error status, so the real cause (embedding misconfiguration, search engine errors, no results) is shown in the chat UI instead of the generic "An error occurred while searching the web". Non-HTTP exceptions keep the generic message, so raw internal error strings are not exposed.

Ref #26750, #25038
2026-07-27 02:07:14 -04:00
Timothy Jaeryang Baek 304cbe4569 refac 2026-07-27 02:00:04 -04:00
crustopher-lgtm 5efe0951d5 feat: add OpenSERP self-hosted web search backend (#27437)
Add self-hosted OpenSERP as a web search engine option. OpenSERP
provides browser-rendered search across Google, Bing, Yandex, Baidu,
DuckDuckGo, and Ecosia with no API keys required.

- New module: retrieval/web/openserp.py (async, uses aiohttp session pool)
- Config: OPENSERP_BASE_URL env var (defaults to http://localhost:7070)
- Routing: search_web() dispatch for 'openserp' engine
- Follows existing patterns (searxng, brave)

Co-authored-by: crustopher-lgtm <crustopher-lgtm@users.noreply.github.com>
2026-07-26 18:52:08 -04:00
Classic298 f7e7f32102 fix: honor Admin UI web loader settings in get_web_loader (#26749)
Since the config refactor, get_web_loader dispatched on the WEB_LOADER_ENGINE module constant, which is read from the environment once at import time. The engine selected in the Admin UI is stored under web.loader.engine in the config table but was never consulted, so UI-configured loader engines (external, playwright, firecrawl, tavily, microsoft_web_iq) were silently ignored and the built-in SafeWebBaseLoader always fetched pages directly. The same applied to the per-engine settings such as the external web loader URL and API key. This breaks egress-restricted deployments that rely on an external web loader: pages are fetched directly from the container and fail with errors like "Network is unreachable" even though an external loader is configured.

Pass the DB-backed loader settings into get_web_loader from both call sites, web search in process_web_search and web fetch via get_loader, and resolve every engine setting from them, keeping the module-level env constants as the fallback for keys that were never saved. Also initialise WebLoaderClass so an unknown engine raises the intended ValueError instead of an UnboundLocalError.

Fixes #26747
2026-07-24 01:30:47 -05:00
Timothy Jaeryang Baek 33cf3fbb7f refac 2026-07-24 01:13:04 -04:00
Timothy Jaeryang Baek 429f2df50c refac 2026-07-23 21:29:33 -04:00
Timothy Jaeryang Baek 89709f5f80 refac 2026-06-29 13:39:08 -05:00
Timothy Jaeryang Baek 517cd8d102 refac 2026-06-29 13:03:14 -05:00
Timothy Jaeryang Baek bb6b2db88b refac 2026-06-29 12:26:23 -05:00
G30 8c7428122b fix(rag): allow special tokens in document text during chunk measurement (#26210)
Documents (especially AI/LLM documentation) legitimately contain special token strings like <|endoftext|> as literal text. The tiktoken encoder raises a ValueError when encountering these during chunk size measurement in merge_docs_to_target_size(), preventing the entire file from being indexed. Pass disallowed_special=() to encoding.encode() to treat all text as normal content.
2026-06-29 10:43:24 -05:00
Timothy Jaeryang Baek 0883638027 refac 2026-06-29 05:47:21 -05:00
Timothy Jaeryang Baek 396d9ac181 refac 2026-06-29 05:27:51 -05:00
Timothy Jaeryang Baek e5b5e5917b refac 2026-06-29 04:43:08 -05:00
Timothy Jaeryang Baek 3fd0384ffc refac 2026-06-29 03:21:41 -05:00
Timothy Jaeryang Baek ce4a323f43 refac 2026-06-29 01:52:07 -05:00
Timothy Jaeryang Baek 7292cee868 refac 2026-06-29 00:42:39 -05:00
Timothy Jaeryang Baek 3a232f5e9a refac 2026-06-28 23:20:54 -05:00
Timothy Jaeryang Baek b5c43968db refac 2026-06-25 03:31:45 +01:00
Timothy Jaeryang Baek 15c7e37438 refac 2026-06-23 23:13:32 +02:00
Timothy Jaeryang Baek 223f484ded refac 2026-06-22 16:10:19 +02:00
Timothy Jaeryang Baek 5cdcdbaeec refac 2026-06-17 02:52:35 +02:00
Timothy Jaeryang Baek 6fce92aa12 chore: format 2026-06-01 13:56:55 -07:00
Timothy Jaeryang Baek 8644532f5b refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-06-01 12:13:04 -07:00
Timothy Jaeryang Baek d4030a8aa5 refac 2026-05-31 15:10:48 -07:00
Timothy Jaeryang Baek 470a074cd1 refac 2026-05-21 16:56:56 +04:00
Timothy Jaeryang Baek b94245d2ee refac 2026-05-21 16:44:36 +04:00
Timothy Jaeryang Baek 56c0d00e13 enh: linkup 2026-05-19 21:54:38 +04:00
Timothy Jaeryang Baek 6d0295588e refac: modernize type annotations (PEP 604 / PEP 585) 2026-05-12 17:10:15 +09:00
Classic298 d11e06f1b7 fix: prevent redirect-based SSRF and enforce collecton write access (#24524)
* fix: prevent redirect-based SSRF in get_image_base64_from_url

Cohort follow-up to PR #24491. That PR patched three call sites
(SafeWebBaseLoader._scrape, get_content_from_url, load_url_image) to
pass allow_redirects=False on the underlying HTTP client; this fourth
call site in utils/files.py was missed.

get_image_base64_from_url() is invoked from convert_url_images_to_base64
in utils/middleware.py on every /api/chat/completions request whose
message content includes an image_url part. validate_url() is called on
the originally-submitted URL only; the aiohttp session.get() call had
no allow_redirects argument and the shared session pool does not
override the aiohttp default (allow_redirects=True). An authenticated
user sending a chat message with image_url pointing at an attacker host
that 302-redirects to 169.254.169.254 / 127.0.0.1 / RFC1918 reached the
internal target. This is the most reachable variant in the redirect
cluster: no special endpoint, no admin permission, no feature flag.

Apply the same one-line fix as the other three call sites: pass
allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS (defaults to False).

Reported by nayakchinmohan in GHSA-88jq-grjp-jx6f; consolidated under
GHSA-rh5x-h6pp-cjj6.

Co-authored-by: nayakchinmohan <nayakchinmohan@users.noreply.github.com>

* fix: enforce collection write access on process_file endpoint

Cohort follow-up to ba83613ff. That commit added _validate_collection_access
to process_text and process_web (the user-supplied collection_name path)
but missed process_file in the same router.

process_file accepts a user-supplied collection_name and writes the file's
embedded content into that collection via save_docs_to_vector_db. The
file_id is gated by file ownership (line 1562) but collection_name was
unchecked, so an authenticated user could append content from a file they
own into another user's knowledge-base collection by passing the victim's
KB UUID as collection_name. Identical pattern to the process_text and
process_web gaps that ba83613ff closed.

Apply the same one-line gate as the sibling endpoints: when
collection_name is user-supplied (not the default file-{file.id} fallback),
require write access via _validate_collection_access. The shared validator
delegates to filter_accessible_collections, which already correctly
handles file-* prefixes (via has_access_to_file) and KB UUIDs
(via Knowledges.check_access_by_user_id) — admins bypass.

Reported by tenbbughunters (Tenable) in GHSA-4g37-7p2c-38r9 (the
comprehensive write-path filing covering process_text / process_file /
process_web / process_youtube and the _validate_collection_access UUID
root cause), and independently re-identified for the missed process_file
call site by kodareef5 in GHSA-4m74-3cmc-293g.

Co-authored-by: tenbbughunters <tenbbughunters@users.noreply.github.com>
Co-authored-by: kodareef5 <kodareef5@users.noreply.github.com>

* fix: enforce collection write access on process_files_batch endpoint

Cohort follow-up to ba83613ff and the prior process_file fix on this
branch. process_files_batch (line 2604) is the third write endpoint in
the same router that accepts a user-supplied collection_name; it was
covered in the same Tenable filing as process_file and was missed by
the same cohort fix. The endpoint validates per-file ownership at line
2642 but does not check whether the caller has write access to the
target collection_name before save_docs_to_vector_db writes into it
at line 2683-2690 with add=True.

Apply the same one-line gate as the sibling endpoints. Validate only
when collection_name is user-supplied (truthy) so the existing fall
through behavior for the None case is unchanged.

Same Tenable / kodareef5 cohort as the previous commit.

Co-authored-by: tenbbughunters <tenbbughunters@users.noreply.github.com>
Co-authored-by: kodareef5 <kodareef5@users.noreply.github.com>

---------

Co-authored-by: nayakchinmohan <nayakchinmohan@users.noreply.github.com>
Co-authored-by: tenbbughunters <tenbbughunters@users.noreply.github.com>
Co-authored-by: kodareef5 <kodareef5@users.noreply.github.com>
2026-05-11 01:09:15 +09:00
Classic298 203ec29baf chore: remove unauthenticated dead-code GET /api/v1/retrieval/ status endpoint (#24497)
The `get_status()` handler at retrieval.py:263 (`@router.get('/')`) returned
the live RAG pipeline configuration (CHUNK_SIZE, CHUNK_OVERLAP, RAG_TEMPLATE,
RAG_EMBEDDING_ENGINE, RAG_EMBEDDING_MODEL, RAG_RERANKING_MODEL, etc.) without
any authentication dependency, while every adjacent endpoint on the same
router (/embedding, /embedding/update, /config, /config/update) requires
get_admin_user.

Exhaustive search of the repository confirms the endpoint has no callers:

- Frontend (src/): no `RETRIEVAL_API_BASE_URL}/'`-style fetch; the existing
  `getRAGConfig()` in src/lib/apis/retrieval/index.ts targets `/config`,
  not the root, and is the only consumer of admin-level retrieval state.
- Backend self-references: none.
- Cypress e2e (chat, documents, registration, settings): none.
- Backend tests (backend/open_webui/test/): none.
- Build/CI scripts (scripts/): none.
- Direct symbol import of `get_status` from this router: none.

The endpoint is dead code, almost certainly a relic from before the
/config GET split. Removing it has zero UX impact and eliminates the
unauthenticated-config-disclosure surface raised in advisory triage on
GHSA-65pg-qhhw-mxwg. External monitoring scripts that may have hit the
bare root will receive a 404 and can switch to the existing /config
endpoint, which returns the same fields plus the rest of the RAG config
under admin auth.

Surface raised by 0xRyuzak1 in GHSA-65pg-qhhw-mxwg. The advisory was closed
as not-a-vulnerability per SECURITY.md Rule 1 (no security boundary
crossed in default config — RAG_TEMPLATE default is a citation-format
instruction, not a system prompt; no integrity/availability impact); this
removal is independent code-hygiene that aligns the router cohort.

Reported-by: 0xRyuzak1 <https://github.com/0xRyuzak1>
2026-05-09 23:19:14 +09:00
Timothy Jaeryang Baek 6700f7bb72 feat: brave search llm context 2026-05-09 06:34:25 +09:00
Timothy Jaeryang Baek 6082e1adae refac 2026-05-09 04:03:49 +09:00
goodbey857 58bc254809 feat: add PaddleOCR-vl loader support and implement retrieval router infrastructure (#23945)
Co-authored-by: Tim Baek <tim@openwebui.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
2026-04-24 15:19:37 +09:00