Files
OmniRoute/tests/unit/9201-search-proxy-bypass.test.ts
Diego Rodrigues de Sa e Souza df64220087 fix(web-search): bind each search provider attempt to its connection proxy (#9201)
* 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>
2026-08-07 18:09:08 -03:00

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