Files
OmniRoute/tests/unit/upstream-error-passthrough.test.ts
Armin Anton” ∴ 10276821cd Integration: security tier + self-hosted operator blockers (rebased onto v3.8.51) (#10952)
Validated on the resolved merge against the current tip (527da656 + the post-#11281 rebaseline): the single conflict was a comment-only collision in providers/[id]/models/route.ts (kept the tip's #10828-ordering note). Focused suites 125/125 across all 13 touched test files (build-sqlite-stub, cc-compatible, copilot-claude-messages, copilot-gemini-route, executor-github, ghe-copilot, github-copilot-discovery-token, github-copilot-model-discovery, noauth-sibling-7620, provider-header-profiles, provider-models-config, request-log-payloads, upstream-error-passthrough), typecheck:core clean, file-size/changelog-integrity OK. Merged --admin over the inherited 2026-08-23 base-red cluster (#9985) — the reds are proven tip failures (CLI catalog cluster + @testing-library allowlist, being drained by #11280), not from this diff. Note: the rebase means several items the body listed (relay x-relay-path SSRF, /v1/search blocked-providers, #10736 rotation fence, #10903, #10865, #10899, #10916) already landed upstream and are NOT in this delta — the delta is: better-sqlite3 build guard + build heap/worker caps + telemetry-off (#10060 re-derived), credential-echo passthrough refusal + OCR/moderation redaction + call-log key redaction, Copilot CLI 1.0.81-6 wire identity + Claude→/v1/messages name-matched routing + discovery token fix, CC model_not_found 400, compat overrides for no-auth aliases (#7620-pinned). The Copilot wire-identity change is the one to watch in production. Thank you @arminanton — and the ported-author credits in the commit history (@rqzbeh, yidecode, the #10899/#10916 authors) are preserved. Your config-posture finding (REQUIRE_API_KEY default vs 0.0.0.0) is noted for a maintainer decision, as you scoped it.
2026-08-23 16:51:25 -03:00

157 lines
5.9 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
shouldPassthroughUpstreamError,
buildPassthroughErrorResponse,
} from "../../open-sse/utils/upstreamErrorPassthrough.ts";
test("upstream error passthrough", async (t) => {
await t.test("4xx com corpo JSON de erro do provider é elegível", () => {
const body = {
type: "error",
error: { type: "invalid_request_error", message: "thinking.type: adaptive is not supported" },
};
assert.equal(shouldPassthroughUpstreamError(400, body), true);
});
await t.test("5xx NÃO é elegível (segue sanitizado)", () => {
assert.equal(shouldPassthroughUpstreamError(500, { error: { message: "x" } }), false);
});
await t.test("corpo com cara de vazamento interno (stack trace) NÃO é elegível", () => {
assert.equal(
shouldPassthroughUpstreamError(400, {
error: { message: "Error\n at /usr/lib/node_modules/omniroute/x.js:1" },
}),
false
);
});
await t.test(
"401/407 NÃO são elegíveis (credencial nossa pode vazar em www-authenticate)",
() => {
assert.equal(shouldPassthroughUpstreamError(401, { error: { message: "bad key" } }), false);
}
);
await t.test(
"corpo que ecoa uma credencial (Bearer/api_key/sk-) NÃO é elegível (#secret-leak hardening)",
() => {
// Some providers echo the offending request inside a 400/422 validation
// body. Passthrough must refuse so the key is not relayed to the client.
assert.equal(
shouldPassthroughUpstreamError(400, {
error: { message: "invalid request: Authorization: Bearer sk-live-abc123def456ghi" },
}),
false
);
assert.equal(
shouldPassthroughUpstreamError(422, {
error: { message: "bad field", received: { api_key: "sk-abc123def456" } },
}),
false
);
assert.equal(
shouldPassthroughUpstreamError(429, {
error: { message: 'rejected: {"api-key":"xyzabc123secret"}' },
}),
false
);
}
);
await t.test(
"corpo de capacidade/quota sem segredo continua elegível (contrato Claude Code preservado)",
() => {
// The common case must still relay verbatim so Claude Code can match the
// wording to auto-disable capabilities.
assert.equal(
shouldPassthroughUpstreamError(400, {
error: { message: "thinking.type: adaptive is not supported" },
}),
true
);
assert.equal(
shouldPassthroughUpstreamError(429, {
error: { type: "rate_limit_error", message: "slow down, retry after 60s" },
}),
true
);
}
);
await t.test("buildPassthroughErrorResponse preserva corpo byte-a-byte", async () => {
const body = {
type: "error",
error: { type: "invalid_request_error", message: "thinking.type: nope" },
};
const res = buildPassthroughErrorResponse(400, body);
assert.ok(res);
assert.equal(res.status, 400);
assert.deepEqual(await res.json(), body);
});
await t.test("retorna null quando inelegível", () => {
assert.equal(buildPassthroughErrorResponse(500, {}), null);
});
});
test("createErrorResult opt-in passthrough (opts.passthrough)", async (t) => {
await t.test(
"com opts.passthrough e corpo elegível, result.response é o corpo upstream verbatim",
async () => {
const { createErrorResult } = await import("../../open-sse/utils/error.ts");
const upstreamBody = {
type: "error",
error: {
type: "invalid_request_error",
message: "thinking.type: adaptive is not supported",
},
};
const result = createErrorResult(400, "msg", null, "code", "type", upstreamBody, {
passthrough: true,
});
assert.deepEqual(await result.response.json(), upstreamBody);
assert.equal(result.status, 400);
// Internal classification fields must never be affected by passthrough.
assert.equal(typeof result.error, "string");
assert.notEqual(result.error, JSON.stringify(upstreamBody));
}
);
await t.test("sem opts, comportamento atual (corpo sanitizado) é preservado", async () => {
const { createErrorResult } = await import("../../open-sse/utils/error.ts");
const upstreamBody = {
type: "error",
error: { type: "invalid_request_error", message: "thinking.type: adaptive is not supported" },
};
const result = createErrorResult(400, "msg", null, "code", "type", upstreamBody);
const body = (await result.response.json()) as { error?: { message?: string } };
assert.ok(body.error?.message, "sanitized body keeps the wrapped error.message shape");
assert.ok(
!JSON.stringify(body).includes(" at /"),
"sanitized body never leaks stack-trace-like text"
);
});
await t.test("com retryAfterMs e passthrough elegível, header Retry-After é setado", async () => {
const { createErrorResult } = await import("../../open-sse/utils/error.ts");
const upstreamBody = {
type: "error",
error: { type: "rate_limit_error", message: "slow down" },
};
const result = createErrorResult(429, "msg", 5000, "code", "type", upstreamBody, {
passthrough: true,
});
assert.equal(result.response.headers.get("Retry-After"), "5");
assert.deepEqual(await result.response.json(), upstreamBody);
});
await t.test(
"opts.passthrough true mas corpo inelegível (401) cai no corpo sanitizado atual",
async () => {
const { createErrorResult } = await import("../../open-sse/utils/error.ts");
const upstreamBody = { error: { message: "bad key" } };
const result = createErrorResult(401, "unauthorized", null, "code", "type", upstreamBody, {
passthrough: true,
});
const body = (await result.response.json()) as { error?: { message?: string } };
assert.notDeepEqual(body, upstreamBody);
assert.ok(body.error?.message, "sanitized body keeps the wrapped error.message shape");
}
);
});