mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
fix(search): name /v1/search 502 provider and cause (#10756)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
This commit is contained in:
1
changelog.d/fixes/10735-search-provider-named-errors.md
Normal file
1
changelog.d/fixes/10735-search-provider-named-errors.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(search):** name `/v1/search` 502s with provider id and sanitized Node cause code, without hostnames ([#10735](https://github.com/diegosouzapw/OmniRoute/issues/10735))
|
||||
@@ -31,6 +31,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
||||
import { z } from "zod";
|
||||
import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
import { resolveSearchProxy, executeProviderFetch } from "./search/searchProxy.ts";
|
||||
import { formatSearchProviderFailure } from "./search/providerFailure.ts";
|
||||
|
||||
export interface SearchResult {
|
||||
title: string;
|
||||
@@ -1177,11 +1178,7 @@ async function tryZaiMCPProvider(
|
||||
/* non-critical — logging must not block search response */
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: isTimeout ? 504 : 502,
|
||||
error: `Search provider ${isTimeout ? "timeout" : "error"}: ${sanitizeErrorMessage(err.message)}`,
|
||||
};
|
||||
return formatSearchProviderFailure(config.id, err, isTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
open-sse/handlers/search/providerFailure.ts
Normal file
26
open-sse/handlers/search/providerFailure.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { sanitizeErrorMessage } from "../../utils/error.ts";
|
||||
|
||||
export interface SearchProviderFailure {
|
||||
success: false;
|
||||
status: number;
|
||||
error: string;
|
||||
}
|
||||
|
||||
/** Named 502/504 for /v1/search — provider id + sanitized cause, no hostnames/URLs. */
|
||||
export function formatSearchProviderFailure(
|
||||
providerId: string,
|
||||
err: unknown,
|
||||
isTimeout: boolean
|
||||
): SearchProviderFailure {
|
||||
const rec = err && typeof err === "object" ? (err as Record<string, unknown>) : {};
|
||||
const cause = rec.cause && typeof rec.cause === "object" ? (rec.cause as Record<string, unknown>) : {};
|
||||
const code =
|
||||
typeof cause.code === "string" && /^[A-Z][A-Z0-9_]{1,39}$/.test(cause.code) ? cause.code : "";
|
||||
const msg =
|
||||
sanitizeErrorMessage(typeof rec.message === "string" ? rec.message : "fetch failed") || "fetch failed";
|
||||
return {
|
||||
success: false,
|
||||
status: isTimeout ? 504 : 502,
|
||||
error: `Search provider ${providerId} ${isTimeout ? "timeout" : "error"}: ${code ? `${msg} (cause: ${code})` : msg}`,
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sanitizeErrorMessage } from "../../utils/error.ts";
|
||||
import { formatSearchProviderFailure } from "./providerFailure.ts";
|
||||
import type { SearchProviderConfig } from "../../config/searchRegistry.ts";
|
||||
import type { SearchResult } from "../search.ts";
|
||||
|
||||
@@ -231,15 +232,12 @@ export async function executeProviderFetch(p: ExecuteProviderFetchParams): Promi
|
||||
clearTimeout(timer);
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
const isTimeout = error.name === "AbortError";
|
||||
const safeMsg = sanitizeErrorMessage(error.message) || "fetch failed";
|
||||
if (log) {
|
||||
log.error("SEARCH", `${config.id} ${isTimeout ? "timeout" : "fetch error"}: ${error.message}`);
|
||||
log.error("SEARCH", `${config.id} ${isTimeout ? "timeout" : "fetch error"}: ${safeMsg}`);
|
||||
}
|
||||
logCall({ status: isTimeout ? 504 : 502, duration: Date.now() - startTime, error: error.message });
|
||||
logCall({ status: isTimeout ? 504 : 502, duration: Date.now() - startTime, error: safeMsg });
|
||||
await emitEvent(isTimeout ? "timeout" : "error");
|
||||
return {
|
||||
success: false,
|
||||
status: isTimeout ? 504 : 502,
|
||||
error: `Search provider ${isTimeout ? "timeout" : "error"}: ${sanitizeErrorMessage(error.message)}`,
|
||||
};
|
||||
return formatSearchProviderFailure(config.id, error, isTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
73
tests/unit/search-provider-named-errors.test.ts
Normal file
73
tests/unit/search-provider-named-errors.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-search-named-errors-"));
|
||||
|
||||
const { handleSearch } = await import("../../open-sse/handlers/search.ts");
|
||||
const { formatSearchProviderFailure } = await import(
|
||||
"../../open-sse/handlers/search/providerFailure.ts"
|
||||
);
|
||||
|
||||
test("formatSearchProviderFailure names the provider and sanitized Node cause", () => {
|
||||
const err = new TypeError("fetch failed");
|
||||
(err as Error & { cause?: { code: string; address: string } }).cause = {
|
||||
code: "ENETUNREACH",
|
||||
address: "203.0.113.10",
|
||||
};
|
||||
|
||||
const result = formatSearchProviderFailure("serper-search", err, false);
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
assert.equal(
|
||||
result.error,
|
||||
"Search provider serper-search error: fetch failed (cause: ENETUNREACH)"
|
||||
);
|
||||
assert.equal(result.error.includes("203.0.113"), false);
|
||||
});
|
||||
|
||||
test("formatSearchProviderFailure omits non-Node cause codes", () => {
|
||||
const err = new TypeError("fetch failed");
|
||||
(err as Error & { cause?: { code: string } }).cause = { code: "not-a-node-code" };
|
||||
|
||||
const result = formatSearchProviderFailure("brave-search", err, false);
|
||||
|
||||
assert.equal(result.status, 502);
|
||||
assert.equal(result.error, "Search provider brave-search error: fetch failed");
|
||||
});
|
||||
|
||||
test("handleSearch names the provider and sanitized cause on fetch failed 502", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => {
|
||||
const err = new TypeError("fetch failed");
|
||||
(err as Error & { cause?: { code: string; address: string } }).cause = {
|
||||
code: "ENETUNREACH",
|
||||
address: "203.0.113.10",
|
||||
};
|
||||
throw err;
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleSearch({
|
||||
query: "named provider 502",
|
||||
provider: "serper-search",
|
||||
maxResults: 5,
|
||||
searchType: "web",
|
||||
credentials: { apiKey: "test-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
assert.equal(
|
||||
result.error,
|
||||
"Search provider serper-search error: fetch failed (cause: ENETUNREACH)"
|
||||
);
|
||||
assert.equal(String(result.error).includes("203.0.113"), false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user