mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 05:02:15 +03:00
* fix(web-search): bind each search provider attempt to its connection proxy (#9201) The search path resolved credentials but never resolved the connection proxy, so the upstream fetch always egressed directly. The connection-test path already used the proxy correctly, proving the gap was in the data-plane transport binding. - Resolve the connection proxy before each upstream attempt using the existing resolveProxyForConnection(connectionId, apiKeyId, providerId) precedence chain, then wrap the fetch in runWithProxyContext so the patched globalThis.fetch routes through the configured proxy. - Resolve and bind the alternate connection proxy independently during failover, so the primary account's context never leaks into the fallback. - Carry connectionId and apiKeyId through SearchHandlerOptions into the route and executeWebSearch callers. - Add connectionId to all saveCallLog entries in tryProvider, so the regular call log identifies the account. - Emit a sanitized logProxyEvent per real upstream search attempt with provider, connection ID, proxy level, status, duration, and target origin/path (no query, API key, or proxy credentials). - Cover both POST /v1/search and executeWebSearch() consumers (MCP, internal, skills) since both bypassed the same proxy binding. * fix(sse): extract search proxy binding into leaf module to fit file-size cap Move the per-attempt proxy resolution, proxied fetch, sanitized proxy-event emission, and response handling for web search providers out of open-sse/handlers/search.ts into a new open-sse/handlers/search/searchProxy.ts, so the provider-dispatch chokepoint (tryProvider) stays a thin wiring call and search.ts fits back under the frozen file-size cap (1536 lines). --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
138 lines
4.4 KiB
TypeScript
138 lines
4.4 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import http from "node:http";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-9201-search-proxy-"));
|
|
process.env.DATA_DIR = dataDir;
|
|
process.env.REQUIRE_API_KEY = "false";
|
|
process.env.DASHBOARD_PASSWORD = "";
|
|
process.env.INITIAL_PASSWORD = "";
|
|
delete process.env.JWT_SECRET;
|
|
delete process.env.HTTP_PROXY;
|
|
delete process.env.HTTPS_PROXY;
|
|
delete process.env.ALL_PROXY;
|
|
delete process.env.http_proxy;
|
|
delete process.env.https_proxy;
|
|
delete process.env.all_proxy;
|
|
process.env.NO_PROXY = "";
|
|
process.env.no_proxy = "";
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const providersDb = await import("../../src/lib/db/providers.ts");
|
|
const proxiesDb = await import("../../src/lib/db/proxies.ts");
|
|
const searchRegistry = await import("../../open-sse/config/searchRegistry.ts");
|
|
const searchRoute = await import("../../src/app/api/v1/search/route.ts");
|
|
|
|
let proxyServer: http.Server;
|
|
let proxyPort = 0;
|
|
let connectionId = "";
|
|
const originalSerperBaseUrl = searchRegistry.SEARCH_PROVIDERS["serper-search"].baseUrl;
|
|
|
|
function listen(server: http.Server): Promise<number> {
|
|
return new Promise((resolve) => {
|
|
server.listen(0, "127.0.0.1", () => {
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") throw new Error("proxy did not bind");
|
|
resolve(address.port);
|
|
});
|
|
});
|
|
}
|
|
|
|
test.before(async () => {
|
|
proxyServer = http.createServer();
|
|
proxyPort = await listen(proxyServer);
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "serper-search",
|
|
authType: "apikey",
|
|
name: "serper-proxy-probe",
|
|
apiKey: "probe-serper-key",
|
|
isActive: true,
|
|
testStatus: "active",
|
|
});
|
|
connectionId = String(connection.id);
|
|
await proxiesDb.createProxyAndAssign(
|
|
{ name: "search-probe-proxy", type: "http", host: "127.0.0.1", port: proxyPort },
|
|
{ scope: "account", scopeId: connectionId }
|
|
);
|
|
|
|
searchRegistry.SEARCH_PROVIDERS["serper-search"].baseUrl = "http://search-probe.invalid";
|
|
});
|
|
|
|
test.after(async () => {
|
|
searchRegistry.SEARCH_PROVIDERS["serper-search"].baseUrl = originalSerperBaseUrl;
|
|
await new Promise<void>((resolve) => proxyServer.close(() => resolve()));
|
|
core.resetDbInstance();
|
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function installProxyResponseCounter() {
|
|
let proxyRequests = 0;
|
|
const payload = JSON.stringify({
|
|
organic: [
|
|
{
|
|
title: "Proxy-served result",
|
|
link: "https://example.com/proxy-served",
|
|
snippet: "The configured connection proxy received this request.",
|
|
},
|
|
],
|
|
searchParameters: { totalResults: 1 },
|
|
});
|
|
proxyServer.removeAllListeners("request");
|
|
proxyServer.removeAllListeners("connect");
|
|
proxyServer.on("request", (_request, response) => {
|
|
proxyRequests += 1;
|
|
response.statusCode = 200;
|
|
response.setHeader("content-type", "application/json");
|
|
response.end(payload);
|
|
});
|
|
proxyServer.on("connect", (_request, socket, head) => {
|
|
proxyRequests += 1;
|
|
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
const reply = () => {
|
|
socket.end(
|
|
`HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ${Buffer.byteLength(payload)}\r\nConnection: close\r\n\r\n${payload}`
|
|
);
|
|
};
|
|
if (head.length > 0) reply();
|
|
else socket.once("data", reply);
|
|
});
|
|
return () => proxyRequests;
|
|
}
|
|
|
|
async function postSearch(query: string) {
|
|
return searchRoute.POST(
|
|
new Request("http://localhost/v1/search", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({
|
|
query,
|
|
provider: "serper-search",
|
|
max_results: 1,
|
|
search_type: "web",
|
|
}),
|
|
})
|
|
);
|
|
}
|
|
|
|
test("POST /v1/search sends a connection's provider request through its configured proxy", async () => {
|
|
const getProxyRequests = installProxyResponseCounter();
|
|
|
|
const response = await postSearch(`proxy probe red ${Date.now()}`);
|
|
const body = (await response.json()) as { results?: unknown[]; error?: unknown };
|
|
|
|
assert.deepEqual(
|
|
{
|
|
status: response.status,
|
|
proxyRequests: getProxyRequests(),
|
|
resultCount: Array.isArray(body.results) ? body.results.length : 0,
|
|
},
|
|
{ status: 200, proxyRequests: 1, resultCount: 1 },
|
|
JSON.stringify(body)
|
|
);
|
|
assert.equal(connectionId.length > 0, true);
|
|
});
|