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 0000000000..a317979b4a --- /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 9230fa1b0e..9043bf38e7 100644 --- a/open-sse/config/searchRegistry.ts +++ b/open-sse/config/searchRegistry.ts @@ -327,6 +327,24 @@ export function resolveSearchProviderId(providerId: string): string { * Request routing should use resolveSearchProvider() so aliases work * without colliding with the Foundation jina-ai provider 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 ca6221538a..be52413ad4 100644 --- a/open-sse/handlers/search.ts +++ b/open-sse/handlers/search.ts @@ -18,7 +18,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 { type FirecrawlSearchEnvelope } from "./search/firecrawlSearch.ts"; @@ -1309,7 +1313,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); +});