From 3c4d8435c42e1ee04f9defd31c4c589017f524c5 Mon Sep 17 00:00:00 2001 From: KRATOS <84986124+gnanam1990@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:28:20 +0530 Subject: [PATCH] fix: surface actionable error when DuckDuckGo web search is rate-limited (#834) Non-Anthropic / non-codex providers (minimax, kimi, generic OpenAI-compatible) fell through to the DDG adapter when no paid search key was configured. DDG's scraper is blocked on most IPs, so web_search surfaced an opaque "anomaly in the request" error. Catch that response in the DDG provider and rethrow with the exact env vars that would unblock the tool, or the option to switch to a native-search provider. Co-authored-by: Claude Opus 4.7 (1M context) --- .../WebSearchTool/providers/duckduckgo.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/tools/WebSearchTool/providers/duckduckgo.ts b/src/tools/WebSearchTool/providers/duckduckgo.ts index 73dafd6b..a21dbe5f 100644 --- a/src/tools/WebSearchTool/providers/duckduckgo.ts +++ b/src/tools/WebSearchTool/providers/duckduckgo.ts @@ -1,6 +1,23 @@ import type { SearchInput, SearchProvider } from './types.js' import { applyDomainFilters, type ProviderOutput } from './types.js' +// DuckDuckGo's HTML scraper aggressively blocks datacenter / repeat IPs with +// an "anomaly in the request" response. When that happens we surface an +// actionable error instead of the opaque scraper message so users know how +// to configure a working backend. +const DDG_ANOMALY_HINT = + 'DuckDuckGo scraping is rate-limited from this network. ' + + 'Configure a search backend with one of: ' + + 'FIRECRAWL_API_KEY, TAVILY_API_KEY, EXA_API_KEY, YOU_API_KEY, ' + + 'JINA_API_KEY, BING_API_KEY, MOJEEK_API_KEY, LINKUP_API_KEY — ' + + 'or use an Anthropic / Vertex / Foundry provider for native web search.' + +function isAnomalyError(message: string): boolean { + return /anomaly in the request|likely making requests too quickly/i.test( + message, + ) +} + export const duckduckgoProvider: SearchProvider = { name: 'duckduckgo', @@ -20,7 +37,16 @@ export const duckduckgoProvider: SearchProvider = { } if (signal?.aborted) throw new DOMException('Aborted', 'AbortError') // TODO: duck-duck-scrape doesn't accept AbortSignal — can't cancel in-flight searches - const response = await search(input.query, { safeSearch: SafeSearchType.STRICT }) + let response: Awaited> + try { + response = await search(input.query, { safeSearch: SafeSearchType.STRICT }) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + if (isAnomalyError(msg)) { + throw new Error(DDG_ANOMALY_HINT) + } + throw err + } const hits = applyDomainFilters( response.results.map(r => ({