Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/10976-skip-default-searxng.md
Original file line number Diff line number Diff line change
@@ -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))
18 changes: 18 additions & 0 deletions open-sse/config/searchRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,24 @@ export const SEARCH_CREDENTIAL_FALLBACKS: Record<string, string> = {
/**
* 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;
}
Expand Down
38 changes: 36 additions & 2 deletions open-sse/handlers/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -1245,7 +1249,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,
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/v1/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getSearchProvider,
selectProvider,
supportsSearchType,
isUnconfiguredLoopbackSearchProvider,
SEARCH_PROVIDERS,
SEARCH_CREDENTIAL_FALLBACKS,
} from "@omniroute/open-sse/config/searchRegistry.ts";
Expand Down Expand Up @@ -248,6 +249,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)) {
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/searxng-loopback-default.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading