fix(search): skip catalog-default SearXNG localhost:8888 (#10981)

5 — Default do catálogo searxng-search (localhost:8888) sempre falha em Docker/K8s, queima o próximo provider (Brave free-tier 429). Agora pula o default não-configurado a menos que baseUrl tenha sido sobrescrito; fallback duckduckgo-free continua disponível. 3/3 testes. Fecha #10976.
This commit is contained in:
Ravi Tharuma
2026-08-21 19:00:35 +02:00
committed by GitHub
parent 3f0b4caa98
commit 143fd78a1a
5 changed files with 87 additions and 2 deletions

View File

@@ -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))

View File

@@ -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;
}

View File

@@ -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<Searc
}
}
// 4. Try primary provider
// 4. Try primary provider (skip catalog-default SearXNG localhost:8888)
if (isUnconfiguredLoopbackSearchProvider(primaryConfig)) {
if (log) {
log.warn(
"SEARCH",
"skipping catalog-default searxng-search at http://localhost:8888/search; set a real SearXNG URL"
);
}
if (
alternateConfig &&
alternateCredentials &&
!isUnconfiguredLoopbackSearchProvider(alternateConfig)
) {
return tryProvider(
alternateConfig,
requestParams,
alternateCredentials,
startTime,
log,
alternateCredentials?.connectionId,
apiKeyId
);
}
return {
success: false,
status: 503,
error:
"SearXNG is still on the catalog default http://localhost:8888/search. Configure a real SearXNG URL or disable the provider.",
};
}
const result = await tryProvider(
primaryConfig,
requestParams,

View File

@@ -10,6 +10,7 @@ import {
resolveSearchProvider,
selectProvider,
supportsSearchType,
isUnconfiguredLoopbackSearchProvider,
SEARCH_PROVIDERS,
SEARCH_CREDENTIAL_FALLBACKS,
} from "@omniroute/open-sse/config/searchRegistry.ts";
@@ -286,6 +287,7 @@ async function postHandler(request: Request, context: unknown) {
if (!alternateProviderId) {
for (const provider of Object.values(SEARCH_PROVIDERS)) {
if (!provider.fallbackOnly || provider.id === providerConfig.id) continue;
if (isUnconfiguredLoopbackSearchProvider(provider)) continue;
if (!supportsSearchType(provider, body.search_type)) continue;
const fallbackCreds = await resolveSearchExecutionCredentials(provider);
if (fallbackCreds && !isAllRateLimitedCredentials(fallbackCreds)) {

View File

@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
SEARCH_PROVIDERS,
isUnconfiguredLoopbackSearchProvider,
} from "../../open-sse/config/searchRegistry.ts";
test("catalog searxng-search default is an unconfigured loopback URL", () => {
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);
});