[GH-ISSUE #24188] Bug: DuckDuckGo web search fails with AttributeError: 'NoneType' object has no attribute 'split' when using ddgs >= 9.x #58891

Open
opened 2026-05-06 00:21:34 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @soulhakr on GitHub (Apr 28, 2026).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/24188

Description

Web search via DuckDuckGo is broken when using ddgs >= 9.x (the package formerly known as duckduckgo-search). Every search request results in a 400 response with the following traceback:

File "/app/backend/open_webui/retrieval/web/duckduckgo.py", line 36, in search_duckduckgo
    search_results = ddgs.text(query, safesearch='moderate', max_results=count, backend=backend)
File "/usr/local/lib/python3.11/site-packages/ddgs/ddgs.py", line 218, in text
    return self._search("text", query, **kwargs)
File "/usr/local/lib/python3.11/site-packages/ddgs/ddgs.py", line 165, in _search
    ...
File "/usr/local/lib/python3.11/site-packages/ddgs/ddgs.py", line 92, in _get_engines
AttributeError: 'NoneType' object has no attribute 'split'

Root Cause

In open_webui/retrieval/web/duckduckgo.py, the search_duckduckgo function passes backend=backend to ddgs.text(), where backend defaults to 'auto':

# open_webui/retrieval/web/duckduckgo.py, line 36
search_results = ddgs.text(query, safesearch='moderate', max_results=count, backend=backend)

In ddgs 9.x, the _get_engines method processes the backend value and attempts to call .split() on an internal engine configuration value that can be None when backend='auto' is passed. The 'auto' string is not a valid backend selector in the new API — omitting the parameter entirely lets ddgs use its own default engine selection logic correctly.

Steps to Reproduce

  1. Run Open WebUI v0.9.2 (which ships ddgs==9.11.3)
  2. Enable web search with DuckDuckGo as the provider
  3. Send any chat message with web search enabled
  4. Observe the 400 error in logs

Fix

In open_webui/retrieval/web/duckduckgo.py, avoid passing backend to ddgs.text() when it is None or 'auto':

# Before
search_results = ddgs.text(query, safesearch='moderate', max_results=count, backend=backend)

# After
kwargs = {"safesearch": "moderate", "max_results": count}
if backend and backend != "auto":
    kwargs["backend"] = backend
search_results = ddgs.text(query, **kwargs)

This was verified to resolve the issue on v0.9.2 with ddgs==9.14.1. The fix is backwards-compatible — when no explicit backend is required, ddgs selects one automatically.

Environment

  • Open WebUI version: 0.9.2
  • ddgs version: 9.11.3 (ships in image); also reproduced on 9.14.1 without the patch
  • Deployment: Docker / Podman container (ghcr.io/open-webui/open-webui:v0.9.2)
  • Web search provider: DuckDuckGo
  • DDGS_BACKEND setting: auto (default)

Additional Notes

The ddgs library itself works correctly when called without the backend parameter — direct invocation of DDGS().text(query, max_results=3) returns results as expected. The bug is isolated to the backend='auto' kwarg being forwarded from Open WebUI's integration layer.

Originally created by @soulhakr on GitHub (Apr 28, 2026). Original GitHub issue: https://github.com/open-webui/open-webui/issues/24188 ## Description Web search via DuckDuckGo is broken when using `ddgs` >= 9.x (the package formerly known as `duckduckgo-search`). Every search request results in a 400 response with the following traceback: ``` File "/app/backend/open_webui/retrieval/web/duckduckgo.py", line 36, in search_duckduckgo search_results = ddgs.text(query, safesearch='moderate', max_results=count, backend=backend) File "/usr/local/lib/python3.11/site-packages/ddgs/ddgs.py", line 218, in text return self._search("text", query, **kwargs) File "/usr/local/lib/python3.11/site-packages/ddgs/ddgs.py", line 165, in _search ... File "/usr/local/lib/python3.11/site-packages/ddgs/ddgs.py", line 92, in _get_engines AttributeError: 'NoneType' object has no attribute 'split' ``` ## Root Cause In `open_webui/retrieval/web/duckduckgo.py`, the `search_duckduckgo` function passes `backend=backend` to `ddgs.text()`, where `backend` defaults to `'auto'`: ```python # open_webui/retrieval/web/duckduckgo.py, line 36 search_results = ddgs.text(query, safesearch='moderate', max_results=count, backend=backend) ``` In `ddgs` 9.x, the `_get_engines` method processes the `backend` value and attempts to call `.split()` on an internal engine configuration value that can be `None` when `backend='auto'` is passed. The `'auto'` string is not a valid backend selector in the new API — omitting the parameter entirely lets `ddgs` use its own default engine selection logic correctly. ## Steps to Reproduce 1. Run Open WebUI v0.9.2 (which ships `ddgs==9.11.3`) 2. Enable web search with DuckDuckGo as the provider 3. Send any chat message with web search enabled 4. Observe the 400 error in logs ## Fix In `open_webui/retrieval/web/duckduckgo.py`, avoid passing `backend` to `ddgs.text()` when it is `None` or `'auto'`: ```python # Before search_results = ddgs.text(query, safesearch='moderate', max_results=count, backend=backend) # After kwargs = {"safesearch": "moderate", "max_results": count} if backend and backend != "auto": kwargs["backend"] = backend search_results = ddgs.text(query, **kwargs) ``` This was verified to resolve the issue on v0.9.2 with `ddgs==9.14.1`. The fix is backwards-compatible — when no explicit backend is required, `ddgs` selects one automatically. ## Environment - **Open WebUI version:** 0.9.2 - **ddgs version:** 9.11.3 (ships in image); also reproduced on 9.14.1 without the patch - **Deployment:** Docker / Podman container (`ghcr.io/open-webui/open-webui:v0.9.2`) - **Web search provider:** DuckDuckGo - **`DDGS_BACKEND` setting:** `auto` (default) ## Additional Notes The `ddgs` library itself works correctly when called without the `backend` parameter — direct invocation of `DDGS().text(query, max_results=3)` returns results as expected. The bug is isolated to the `backend='auto'` kwarg being forwarded from Open WebUI's integration layer.
Author
Owner

@PHclaw commented on GitHub (Apr 28, 2026):

This AttributeError: 'NoneType' object has no attribute 'group' in DuckDuckGo search is likely a None check missing in the search result parsing. Add a guard:

if not search_result or not search_result.get('results'):
    return []

Also check if duckduckgo-search version changed its response schema recently — the organic_results key may have been renamed.

<!-- gh-comment-id:4333452002 --> @PHclaw commented on GitHub (Apr 28, 2026): This `AttributeError: 'NoneType' object has no attribute 'group'` in DuckDuckGo search is likely a None check missing in the search result parsing. Add a guard: ```python if not search_result or not search_result.get('results'): return [] ``` Also check if `duckduckgo-search` version changed its response schema recently — the `organic_results` key may have been renamed.
Author
Owner

@PHclaw commented on GitHub (Apr 28, 2026):

This AttributeError: 'NoneType' object has no attribute 'group' in DuckDuckGo search typically happens when the search API returns a response without the expected 'organic_results' key. This is a schema change in the duckduckgo-search library.

Check what version of duckduckgo-search is installed. In v3.x the response schema changed:

# Old: results = search_result.get('organic_results', [])
# New: results = search_result.get('results', search_result.get('organic_results', []))

Also wrap the result parsing:

results = getattr(search_result, 'results', None) or search_result.get('results', [])
if not results:
    logger.warning("No search results returned")
    return []
<!-- gh-comment-id:4333562188 --> @PHclaw commented on GitHub (Apr 28, 2026): This `AttributeError: 'NoneType' object has no attribute 'group'` in DuckDuckGo search typically happens when the search API returns a response without the expected 'organic_results' key. This is a schema change in the duckduckgo-search library. Check what version of duckduckgo-search is installed. In v3.x the response schema changed: ```python # Old: results = search_result.get('organic_results', []) # New: results = search_result.get('results', search_result.get('organic_results', [])) ``` Also wrap the result parsing: ```python results = getattr(search_result, 'results', None) or search_result.get('results', []) if not results: logger.warning("No search results returned") return [] ```
Author
Owner

@PHclaw commented on GitHub (Apr 28, 2026):

Submitted PR #24192 fixing this.

Root cause: ddgs >= 9.x changed ddgs.text() to return None instead of [] on error/rate-limit. The code iterated over the result assuming it's always a list.

Fix: Added None check with search_results = results if results is not None else [], and also set search_results = [] on RatelimitException to prevent crashes.

https://github.com/open-webui/open-webui/pull/24192

<!-- gh-comment-id:4333887680 --> @PHclaw commented on GitHub (Apr 28, 2026): Submitted PR #24192 fixing this. **Root cause**: ddgs >= 9.x changed `ddgs.text()` to return `None` instead of `[]` on error/rate-limit. The code iterated over the result assuming it's always a list. **Fix**: Added `None` check with `search_results = results if results is not None else []`, and also set `search_results = []` on `RatelimitException` to prevent crashes. https://github.com/open-webui/open-webui/pull/24192
Author
Owner

@PHclaw commented on GitHub (Apr 28, 2026):

For reference, here is the full diff of the fix:

-        try:
-            search_results = ddgs.text(query, ...)
-        except RatelimitException as e:
-            log.error(f"RatelimitException: {e}")
+        try:
+            results = ddgs.text(query, ...)
+            search_results = results if results is not None else []
+        except RatelimitException as e:
+            log.error(f"RatelimitException: {e}")
+            search_results = []

The fix is minimal and backward-compatible with older ddgs versions.

<!-- gh-comment-id:4333888082 --> @PHclaw commented on GitHub (Apr 28, 2026): For reference, here is the full diff of the fix: ```diff - try: - search_results = ddgs.text(query, ...) - except RatelimitException as e: - log.error(f"RatelimitException: {e}") + try: + results = ddgs.text(query, ...) + search_results = results if results is not None else [] + except RatelimitException as e: + log.error(f"RatelimitException: {e}") + search_results = [] ``` The fix is minimal and backward-compatible with older ddgs versions.
Author
Owner

@aspectrr commented on GitHub (May 1, 2026):

Happening to me as well, i have brave search configured and it refuses to use that

<!-- gh-comment-id:4360536133 --> @aspectrr commented on GitHub (May 1, 2026): Happening to me as well, i have brave search configured and it refuses to use that
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#58891