From 2e3c2a68dfb1d14360f9048de991fc1de4d54711 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Fri, 21 Aug 2026 14:45:06 +0200 Subject: [PATCH] fix(search): skip catalog-default SearXNG localhost:8888 Docker/K8s has no SearXNG on :8888. The catalog default plus OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS made every search ECONNREFUSED, 502, then burn the next provider (Brave 429). Skip http://localhost:8888/search unless the operator overrides baseUrl. Last-resort fallback then reaches duckduckgo-free. Closes #10976. --- .../fixes/10976-skip-default-searxng.md | 1 + open-sse/config/searchRegistry.ts | 18 +++++++++ open-sse/handlers/search.ts | 38 ++++++++++++++++++- src/app/api/v1/search/route.ts | 2 + tests/unit/searxng-loopback-default.test.ts | 30 +++++++++++++++ 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10976-skip-default-searxng.md create mode 100644 tests/unit/searxng-loopback-default.test.ts diff --git a/changelog.d/fixes/10976-skip-default-searxng.md b/changelog.d/fixes/10976-skip-default-searxng.md new file mode 100644 index 000000000000..a317979b4a1c --- /dev/null +++ b/changelog.d/fixes/10976-skip-default-searxng.md @@ -0,0 +1 @@ +- **fix(search):** skip catalog-default SearXNG `http://localhost:8888/search` so Docker/K8s search does not ECONNREFUSED then 502 into the next provider ([#10976](https://github.com/diegosouzapw/OmniRoute/issues/10976)) diff --git a/open-sse/config/searchRegistry.ts b/open-sse/config/searchRegistry.ts index b742ad491421..519a7e21e8da 100644 --- a/open-sse/config/searchRegistry.ts +++ b/open-sse/config/searchRegistry.ts @@ -276,6 +276,24 @@ export const SEARCH_CREDENTIAL_FALLBACKS: Record = { /** * Get search provider config by ID */ +const CATALOG_SEARXNG_DEFAULT_URL = "http://localhost:8888/search"; + +/** + * Catalog default SearXNG URL is a desktop convenience. In Docker/K8s nothing + * listens on :8888, and OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS (needed for + * ClusterIP providers) lets ProxyFetch attempt it, producing ECONNREFUSED and + * a 502 that then burns the next fallback's quota. Skip unless the operator + * overrode baseUrl. + */ +export function isUnconfiguredLoopbackSearchProvider( + provider: SearchProviderConfig | null | undefined +): boolean { + if (!provider || provider.id !== "searxng-search") return false; + const configured = String(provider.baseUrl || "").replace(/\/+$/, ""); + const catalog = CATALOG_SEARXNG_DEFAULT_URL.replace(/\/+$/, ""); + return configured === catalog; +} + export function getSearchProvider(providerId: string): SearchProviderConfig | null { return SEARCH_PROVIDERS[providerId] || null; } diff --git a/open-sse/handlers/search.ts b/open-sse/handlers/search.ts index 366ae0014d9d..e7b006206670 100644 --- a/open-sse/handlers/search.ts +++ b/open-sse/handlers/search.ts @@ -17,7 +17,11 @@ import { randomUUID } from "crypto"; * } */ -import { getSearchProvider, type SearchProviderConfig } from "../config/searchRegistry.ts"; +import { + getSearchProvider, + isUnconfiguredLoopbackSearchProvider, + type SearchProviderConfig, +} from "../config/searchRegistry.ts"; import { buildPerplexityRequest, parsePerplexitySearchOptions } from "./search/perplexitySearch.ts"; import * as fcSearch from "./search/firecrawlSearch.ts"; import { freeWebSearch } from "../services/freeWebSearch.ts"; @@ -1245,7 +1249,37 @@ export async function handleSearch(options: SearchHandlerOptions): Promise { + const searxng = SEARCH_PROVIDERS["searxng-search"]; + assert.ok(searxng); + assert.equal(isUnconfiguredLoopbackSearchProvider(searxng), true); +}); + +test("overridden SearXNG URL is not skipped", () => { + const searxng = SEARCH_PROVIDERS["searxng-search"]; + assert.equal( + isUnconfiguredLoopbackSearchProvider({ + ...searxng, + baseUrl: "http://searxng.inference.svc/search", + }), + false + ); +}); + +test("other fallback providers are not treated as unconfigured loopback", () => { + assert.equal(isUnconfiguredLoopbackSearchProvider(SEARCH_PROVIDERS["duckduckgo-free"]), false); + assert.equal(isUnconfiguredLoopbackSearchProvider(SEARCH_PROVIDERS["brave-search"]), false); + assert.equal(isUnconfiguredLoopbackSearchProvider(null), false); +});