Compare commits

...

2 Commits

4 changed files with 182 additions and 8 deletions

View File

@@ -0,0 +1 @@
- Sanitize HuggingChat conversation-creation and message-send transport failures before they reach client error bodies or provider logs.

View File

@@ -400,13 +400,15 @@ export class HuggingChatExecutor extends BaseExecutor {
};
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
log?.error?.("HUGGINGCHAT", `Conversation creation failed: ${message}`);
return {
response: new Response(
JSON.stringify({
error: { message: `HuggingChat connection failed: ${message}`, type: "upstream_error" },
}),
JSON.stringify(
buildErrorBody(502, `HuggingChat connection failed: ${message}`, undefined, {
type: "upstream_error",
})
),
{ status: 502, headers: { "Content-Type": "application/json" } }
),
url: CONVERSATION_URL,
@@ -463,13 +465,15 @@ export class HuggingChatExecutor extends BaseExecutor {
signal: combinedSignal,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
log?.error?.("HUGGINGCHAT", `Message send failed: ${message}`);
return {
response: new Response(
JSON.stringify({
error: { message: `HuggingChat connection failed: ${message}`, type: "upstream_error" },
}),
JSON.stringify(
buildErrorBody(502, `HuggingChat connection failed: ${message}`, undefined, {
type: "upstream_error",
})
),
{ status: 502, headers: { "Content-Type": "application/json" } }
),
url: messageUrl,

View File

@@ -0,0 +1,85 @@
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const CHILD_RESULT_PREFIX = "HUGGINGCHAT_TRANSPORT_RESULT=";
const scenario = process.argv[2];
if (scenario !== "conversation-creation" && scenario !== "message-send") {
throw new Error(`Unknown HuggingChat transport scenario: ${String(scenario)}`);
}
const testRoot = mkdtempSync(join(tmpdir(), "omniroute-huggingchat-transport-child-"));
const testDataDir = join(testRoot, "data");
const testPluginsDir = join(testRoot, "plugins");
const testConfigDir = join(testRoot, "config");
mkdirSync(testDataDir, { recursive: true });
mkdirSync(testPluginsDir, { recursive: true });
mkdirSync(testConfigDir, { recursive: true });
process.env.DATA_DIR = testDataDir;
process.env.OMNIROUTE_PLUGINS_DIR = testPluginsDir;
process.env.XDG_CONFIG_HOME = testConfigDir;
process.env.APP_LOG_TO_FILE = "false";
process.env.API_KEY_SECRET = "synthetic-huggingchat-transport-test-key";
const hostileTransportMessage =
"TLS request failed at /srv/omniroute/providers/huggingchat/client.ts:44:9 " +
"access_token=transport-secret\n" +
" at sendRequest (/srv/omniroute/runtime/fetch.ts:12:3)";
const originalFetch = globalThis.fetch;
let fetchCalls = 0;
const errorLogs: string[] = [];
let childResult: Record<string, unknown> | null = null;
try {
const { HuggingChatExecutor } = await import("../../../open-sse/executors/huggingchat.ts");
globalThis.fetch = (async () => {
fetchCalls += 1;
if (scenario === "conversation-creation") {
throw new Error(hostileTransportMessage);
}
if (fetchCalls === 1) {
return Response.json({ conversationId: "conversation-test" });
}
if (fetchCalls === 2) {
return Response.json({ rootMessageId: "root-message-test" });
}
if (fetchCalls === 3) {
throw new Error(hostileTransportMessage);
}
throw new Error(`Unexpected fetch call ${fetchCalls}`);
}) as typeof globalThis.fetch;
const result = await new HuggingChatExecutor().execute({
model: "test/huggingchat-model",
body: { messages: [{ role: "user", content: "hello" }] },
stream: false,
credentials: { apiKey: "hf-chat=fake-cookie" },
signal: null,
log: { error: (_tag, message) => errorLogs.push(message) },
});
childResult = {
fetchCalls,
status: result.response.status,
contentType: result.response.headers.get("content-type") || "",
errorLogs,
payload: await result.response.json(),
};
} finally {
globalThis.fetch = originalFetch;
const coreDb = await import("../../../src/lib/db/core.ts");
coreDb.resetDbInstance();
rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
if (!childResult) {
throw new Error(`HuggingChat ${scenario} probe did not produce a result`);
}
process.stdout.write(`${CHILD_RESULT_PREFIX}${JSON.stringify(childResult)}\n`);

View File

@@ -0,0 +1,84 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
const CHILD_RESULT_PREFIX = "HUGGINGCHAT_TRANSPORT_RESULT=";
const childFixture = fileURLToPath(
new URL("./_fixtures/huggingchat-transport-error-child.ts", import.meta.url)
);
type TransportScenario = "conversation-creation" | "message-send";
type TransportFailureResult = {
fetchCalls: number;
status: number;
contentType: string;
errorLogs: string[];
payload: {
error: {
message: string;
type?: string;
code?: string;
};
};
};
function runTransportScenario(scenario: TransportScenario): TransportFailureResult {
const result = spawnSync(process.execPath, ["--import", "tsx/esm", childFixture, scenario], {
cwd: process.cwd(),
encoding: "utf8",
timeout: 60_000,
env: {
NODE_ENV: "test",
NO_COLOR: "1",
DISABLE_SQLITE_AUTO_BACKUP: "true",
},
});
assert.equal(
result.status,
0,
`isolated ${scenario} probe failed: ${String(result.stderr).slice(0, 2_000)}`
);
const resultLine = String(result.stdout)
.split("\n")
.findLast((line) => line.startsWith(CHILD_RESULT_PREFIX));
assert.ok(resultLine, `isolated ${scenario} probe did not emit its result`);
return JSON.parse(resultLine.slice(CHILD_RESULT_PREFIX.length)) as TransportFailureResult;
}
function assertPublicFailureIsSanitized(result: TransportFailureResult): void {
assert.equal(result.status, 502);
assert.match(result.contentType, /application\/json/);
assert.equal(result.payload.error.type, "upstream_error");
assert.match(result.payload.error.message, /^HuggingChat connection failed:/);
assert.match(result.payload.error.message, /<path>/);
assert.match(result.payload.error.message, /access_token=\[REDACTED\]/);
const publicText = JSON.stringify({ payload: result.payload, errorLogs: result.errorLogs });
assert.doesNotMatch(publicText, /transport-secret/);
assert.doesNotMatch(publicText, /\/srv\/omniroute/);
assert.doesNotMatch(publicText, /sendRequest/);
assert.doesNotMatch(publicText, /\n\s*at /);
}
test("HuggingChat sanitizes conversation-creation transport failures in body and log", () => {
const result = runTransportScenario("conversation-creation");
assert.equal(result.fetchCalls, 1, "the probe must intercept the conversation creation request");
assert.equal(result.errorLogs.length, 1);
assert.match(result.errorLogs[0], /^Conversation creation failed:/);
assertPublicFailureIsSanitized(result);
});
test("HuggingChat sanitizes message-send transport failures in body and log", () => {
const result = runTransportScenario("message-send");
assert.equal(result.fetchCalls, 3, "the probe must intercept creation, parent lookup, and send");
assert.equal(result.errorLogs.length, 1);
assert.match(result.errorLogs[0], /^Message send failed:/);
assertPublicFailureIsSanitized(result);
});