mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 06:12:17 +03:00
security(lmarena): close public stream error boundary
This commit is contained in:
@@ -158,11 +158,11 @@ export class LMArenaExecutor extends BaseExecutor {
|
||||
} catch (error) {
|
||||
if (isTlsClientUnavailableError(error)) {
|
||||
log?.error?.("LMArenaExecutor", `TLS client unavailable: ${sanitizeLMArenaError(error)}`);
|
||||
return mapTlsUnavailable(error, url, headers, transformedBody);
|
||||
return mapTlsUnavailable(url, headers, transformedBody);
|
||||
}
|
||||
const publicMessage = sanitizeLMArenaError(error);
|
||||
log?.error?.("LMArenaExecutor", `Request failed: ${publicMessage}`);
|
||||
return mapNetworkError(error, url, headers, transformedBody);
|
||||
const logMessage = sanitizeLMArenaError(error);
|
||||
log?.error?.("LMArenaExecutor", `Request failed: ${logMessage}`);
|
||||
return mapNetworkError(url, headers, transformedBody);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* the executor methods stay under complexity / max-lines gates.
|
||||
*/
|
||||
import { isCloudflareChallenge } from "../../services/lmarenaTlsClient.ts";
|
||||
import { buildErrorBody } from "../../utils/error.ts";
|
||||
import { sanitizeLMArenaError } from "./error.ts";
|
||||
import { markLMArenaCatalogModelDead } from "./models.ts";
|
||||
import { parseArenaSSE } from "./stream.ts";
|
||||
@@ -15,9 +16,133 @@ const SAFE_ARENA_STREAM_ERROR_NAMES = new Set([
|
||||
"BodyTimeoutError",
|
||||
]);
|
||||
|
||||
function projectArenaStreamError(error: unknown, publicMessage: string): Error {
|
||||
const projected = new Error(publicMessage) as Error & { statusCode?: number };
|
||||
type ArenaPublicFailure =
|
||||
| { kind: "missing-cookie" }
|
||||
| {
|
||||
kind: "bot-block";
|
||||
status: number;
|
||||
reason: "cloudflare" | "token-present" | "token-needed";
|
||||
}
|
||||
| { kind: "http-status"; status: number }
|
||||
| { kind: "tls-unavailable" }
|
||||
| { kind: "network" }
|
||||
| { kind: "upstream-event" }
|
||||
| { kind: "stream-transport" };
|
||||
|
||||
interface ArenaProjectedPublicFailure {
|
||||
status: number;
|
||||
message: string;
|
||||
type: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
type ArenaProjectedStreamError = Error & {
|
||||
statusCode?: number;
|
||||
type?: string;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
const ARENA_PUBLIC_MESSAGES = {
|
||||
missingCookie:
|
||||
"Arena requires a session cookie. Paste the full Cookie header from arena.ai (include arena-auth-prod-v1.* chunks and ideally cf_clearance).",
|
||||
cloudflareBot:
|
||||
"Arena blocked by Cloudflare bot management. Use a residential/browser-grade network if needed, paste a fresh full Cookie header (include cf_clearance / __cf_bm when present), and optionally set providerSpecificData.recaptchaV3Token from a live browser session.",
|
||||
botTokenNeeded:
|
||||
"If this persists, supply a browser reCAPTCHA v3 token via credentials.providerSpecificData.recaptchaV3Token (in addition to the session cookie).",
|
||||
tlsUnavailable:
|
||||
"Arena TLS impersonation unavailable: Arena upstream error. Install/repair tls-client-node native binary.",
|
||||
upstream: "Arena upstream error",
|
||||
upstreamStream: "Arena upstream stream error",
|
||||
} as const;
|
||||
|
||||
function normalizeArenaStatus(status: number, fallback: number): number {
|
||||
return Number.isInteger(status) && status >= 200 && status <= 599 ? status : fallback;
|
||||
}
|
||||
|
||||
function projectArenaPublicFailure(failure: ArenaPublicFailure): ArenaProjectedPublicFailure {
|
||||
switch (failure.kind) {
|
||||
case "missing-cookie":
|
||||
return {
|
||||
status: 401,
|
||||
message: ARENA_PUBLIC_MESSAGES.missingCookie,
|
||||
type: "authentication_error",
|
||||
code: "missing_cookie",
|
||||
};
|
||||
case "bot-block": {
|
||||
const status = normalizeArenaStatus(failure.status, 403);
|
||||
let message: string;
|
||||
switch (failure.reason) {
|
||||
case "cloudflare":
|
||||
message = ARENA_PUBLIC_MESSAGES.cloudflareBot;
|
||||
break;
|
||||
case "token-present":
|
||||
message = `Arena API error: ${status}`;
|
||||
break;
|
||||
case "token-needed":
|
||||
message = `Arena API error: ${status}. ${ARENA_PUBLIC_MESSAGES.botTokenNeeded}`;
|
||||
break;
|
||||
default: {
|
||||
const exhaustiveReason: never = failure.reason;
|
||||
return exhaustiveReason;
|
||||
}
|
||||
}
|
||||
return {
|
||||
status,
|
||||
message,
|
||||
type: "api_error",
|
||||
code: "cloudflare_or_bot",
|
||||
};
|
||||
}
|
||||
case "http-status": {
|
||||
const status = normalizeArenaStatus(failure.status, 502);
|
||||
return {
|
||||
status,
|
||||
message: `Arena API error: ${status}`,
|
||||
type: "api_error",
|
||||
code: String(status),
|
||||
};
|
||||
}
|
||||
case "tls-unavailable":
|
||||
return {
|
||||
status: 502,
|
||||
message: ARENA_PUBLIC_MESSAGES.tlsUnavailable,
|
||||
type: "upstream_error",
|
||||
code: "TLS_CLIENT_UNAVAILABLE",
|
||||
};
|
||||
case "network":
|
||||
return {
|
||||
status: 502,
|
||||
message: ARENA_PUBLIC_MESSAGES.upstream,
|
||||
type: "network_error",
|
||||
code: "request_failed",
|
||||
};
|
||||
case "upstream-event":
|
||||
return {
|
||||
status: 502,
|
||||
message: ARENA_PUBLIC_MESSAGES.upstream,
|
||||
type: "api_error",
|
||||
code: "lmarena_error",
|
||||
};
|
||||
case "stream-transport":
|
||||
return {
|
||||
status: 502,
|
||||
message: ARENA_PUBLIC_MESSAGES.upstreamStream,
|
||||
type: "upstream_error",
|
||||
code: "lmarena_stream_error",
|
||||
};
|
||||
default: {
|
||||
const exhaustiveFailure: never = failure;
|
||||
return exhaustiveFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function projectArenaStreamError(error: unknown): Error {
|
||||
const publicError = buildArenaPublicError({ kind: "stream-transport" }).body.error;
|
||||
const projected = new Error(publicError.message) as ArenaProjectedStreamError;
|
||||
projected.stack = undefined;
|
||||
projected.type = publicError.type;
|
||||
projected.code = publicError.code;
|
||||
if (!error || typeof error !== "object") return projected;
|
||||
|
||||
try {
|
||||
@@ -25,9 +150,14 @@ function projectArenaStreamError(error: unknown, publicMessage: string): Error {
|
||||
if (typeof name === "string" && SAFE_ARENA_STREAM_ERROR_NAMES.has(name)) {
|
||||
projected.name = name;
|
||||
}
|
||||
const statusCode = Number((error as { statusCode?: unknown }).statusCode);
|
||||
if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599) {
|
||||
projected.statusCode = statusCode;
|
||||
const rawStatusCode = (error as { statusCode?: unknown }).statusCode;
|
||||
if (
|
||||
typeof rawStatusCode === "number" &&
|
||||
Number.isInteger(rawStatusCode) &&
|
||||
rawStatusCode >= 400 &&
|
||||
rawStatusCode <= 599
|
||||
) {
|
||||
projected.statusCode = rawStatusCode;
|
||||
}
|
||||
} catch {
|
||||
// Hostile thrown values must not escape through coercing metadata accessors.
|
||||
@@ -35,19 +165,23 @@ function projectArenaStreamError(error: unknown, publicMessage: string): Error {
|
||||
return projected;
|
||||
}
|
||||
|
||||
export function errorResponse(
|
||||
status: number,
|
||||
message: string,
|
||||
type: string,
|
||||
code: string
|
||||
): Response {
|
||||
const publicMessage = sanitizeLMArenaError(message);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: { message: publicMessage, type, code },
|
||||
function buildArenaPublicError(failure: ArenaPublicFailure) {
|
||||
const projected = projectArenaPublicFailure(failure);
|
||||
return {
|
||||
status: projected.status,
|
||||
body: buildErrorBody(projected.status, projected.message, undefined, {
|
||||
type: projected.type,
|
||||
code: projected.code,
|
||||
}),
|
||||
{ status, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function errorResponse(failure: ArenaPublicFailure): Response {
|
||||
const projected = buildArenaPublicError(failure);
|
||||
return new Response(JSON.stringify(projected.body), {
|
||||
status: projected.status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
export function missingCookieResult(
|
||||
@@ -56,12 +190,7 @@ export function missingCookieResult(
|
||||
transformedBody: unknown
|
||||
) {
|
||||
return {
|
||||
response: errorResponse(
|
||||
401,
|
||||
"Arena requires a session cookie. Paste the full Cookie header from arena.ai (include arena-auth-prod-v1.* chunks and ideally cf_clearance).",
|
||||
"authentication_error",
|
||||
"missing_cookie"
|
||||
),
|
||||
response: errorResponse({ kind: "missing-cookie" }),
|
||||
url,
|
||||
headers,
|
||||
transformedBody,
|
||||
@@ -74,12 +203,17 @@ function isBotOrChallenge(status: number, text: string | null | undefined): bool
|
||||
return Boolean(text && text.trimStart().startsWith("<!DOCTYPE"));
|
||||
}
|
||||
|
||||
function botBlockMessage(text: string | null | undefined, hasRecaptcha: boolean, status: number) {
|
||||
if (isCloudflareChallenge(text)) {
|
||||
return "Arena blocked by Cloudflare bot management. Use a residential/browser-grade network if needed, paste a fresh full Cookie header (include cf_clearance / __cf_bm when present), and optionally set providerSpecificData.recaptchaV3Token from a live browser session.";
|
||||
}
|
||||
if (hasRecaptcha) return `Arena API error: ${status}`;
|
||||
return `Arena API error: ${status}. If this persists, supply a browser reCAPTCHA v3 token via credentials.providerSpecificData.recaptchaV3Token (in addition to the session cookie).`;
|
||||
function botBlockFailure(
|
||||
text: string | null | undefined,
|
||||
hasRecaptcha: boolean,
|
||||
status: number
|
||||
): ArenaPublicFailure {
|
||||
const reason = isCloudflareChallenge(text)
|
||||
? "cloudflare"
|
||||
: hasRecaptcha
|
||||
? "token-present"
|
||||
: "token-needed";
|
||||
return { kind: "bot-block", status, reason };
|
||||
}
|
||||
|
||||
/** Map non-2xx / CF TLS results to an executor failure payload, or null if OK. */
|
||||
@@ -96,12 +230,7 @@ export function mapFailedTlsResult(opts: {
|
||||
const { status, text, hasRecaptcha, model, arenaModelId, url, headers, transformedBody } = opts;
|
||||
if (isBotOrChallenge(status, text)) {
|
||||
return {
|
||||
response: errorResponse(
|
||||
status || 403,
|
||||
botBlockMessage(text, hasRecaptcha, status),
|
||||
"api_error",
|
||||
"cloudflare_or_bot"
|
||||
),
|
||||
response: errorResponse(botBlockFailure(text, hasRecaptcha, status)),
|
||||
url,
|
||||
headers,
|
||||
transformedBody,
|
||||
@@ -116,7 +245,7 @@ export function mapFailedTlsResult(opts: {
|
||||
// Fail closed: TLS error bodies can contain upstream stacks, causes, or internal identifiers.
|
||||
// Preserve the HTTP classification without projecting any body-derived text to the caller.
|
||||
return {
|
||||
response: errorResponse(status, `Arena API error: ${status}`, "api_error", String(status)),
|
||||
response: errorResponse({ kind: "http-status", status }),
|
||||
url,
|
||||
headers,
|
||||
transformedBody,
|
||||
@@ -124,18 +253,12 @@ export function mapFailedTlsResult(opts: {
|
||||
}
|
||||
|
||||
export function mapTlsUnavailable(
|
||||
error: Error,
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
transformedBody: unknown
|
||||
) {
|
||||
return {
|
||||
response: errorResponse(
|
||||
502,
|
||||
`Arena TLS impersonation unavailable: ${sanitizeLMArenaError(error)}. Install/repair tls-client-node native binary.`,
|
||||
"upstream_error",
|
||||
"TLS_CLIENT_UNAVAILABLE"
|
||||
),
|
||||
response: errorResponse({ kind: "tls-unavailable" }),
|
||||
url,
|
||||
headers,
|
||||
transformedBody,
|
||||
@@ -143,13 +266,12 @@ export function mapTlsUnavailable(
|
||||
}
|
||||
|
||||
export function mapNetworkError(
|
||||
message: unknown,
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
transformedBody: unknown
|
||||
) {
|
||||
return {
|
||||
response: errorResponse(502, sanitizeLMArenaError(message), "network_error", "request_failed"),
|
||||
response: errorResponse({ kind: "network" }),
|
||||
url,
|
||||
headers,
|
||||
transformedBody,
|
||||
@@ -214,10 +336,11 @@ function handleArenaEventLine(
|
||||
return false;
|
||||
}
|
||||
if (event.type === "error") {
|
||||
const upstreamError = buildArenaPublicError({ kind: "upstream-event" });
|
||||
enqueueSse(controller, {
|
||||
...baseChunk(model),
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
error: { message: sanitizeLMArenaError(event.content) },
|
||||
error: upstreamError.body.error,
|
||||
});
|
||||
controller.close();
|
||||
return true;
|
||||
@@ -290,9 +413,9 @@ export function createOpenAIArenaStream(opts: {
|
||||
}
|
||||
emitStopAndDone(controller, model);
|
||||
} catch (error) {
|
||||
const publicMessage = sanitizeLMArenaError(error, "Arena upstream stream error");
|
||||
log?.error?.("LMArenaExecutor", `Streaming error: ${publicMessage}`);
|
||||
controller.error(projectArenaStreamError(error, publicMessage));
|
||||
const logMessage = sanitizeLMArenaError(error, "Arena upstream stream error");
|
||||
log?.error?.("LMArenaExecutor", `Streaming error: ${logMessage}`);
|
||||
controller.error(projectArenaStreamError(error));
|
||||
} finally {
|
||||
await cleanupReader();
|
||||
if (signal) signal.removeEventListener("abort", onAbort);
|
||||
@@ -312,7 +435,7 @@ export async function handleNonStreamingArenaResponse(
|
||||
): Promise<Response> {
|
||||
const text = await response.text();
|
||||
let fullText = "";
|
||||
let error: string | null = null;
|
||||
let hasUpstreamError = false;
|
||||
|
||||
for (const line of text.split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
@@ -321,12 +444,14 @@ export async function handleNonStreamingArenaResponse(
|
||||
if (!event) continue;
|
||||
if (event.type === "text" && event.content) fullText += event.content;
|
||||
else if (event.type === "error") {
|
||||
error = event.content || "Unknown error";
|
||||
hasUpstreamError = true;
|
||||
break;
|
||||
} else if (event.type === "done") break;
|
||||
}
|
||||
|
||||
if (error) return errorResponse(502, error, "api_error", "lmarena_error");
|
||||
if (hasUpstreamError) {
|
||||
return errorResponse({ kind: "upstream-event" });
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -647,6 +647,36 @@ describe("LMArena Executor", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("normalizes an invalid upstream status before building the public error", async () => {
|
||||
__setTlsFetchOverrideForTesting(async () => ({
|
||||
status: 700,
|
||||
headers: new Headers({ "Content-Type": "text/plain" }),
|
||||
text: "InvalidStatusInternalFailure secret-status-id",
|
||||
body: null,
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await new LMArenaExecutor().execute({
|
||||
model: TEST_ARENA_MODEL_ID,
|
||||
body: { messages: [{ role: "user", content: "Hello" }] },
|
||||
credentials: { cookie: "session=test" },
|
||||
signal: new AbortController().signal,
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 502);
|
||||
const responseText = await result.response.text();
|
||||
assert.deepEqual(JSON.parse(responseText).error, {
|
||||
message: "Arena API error: 502",
|
||||
type: "api_error",
|
||||
code: "502",
|
||||
});
|
||||
assert.doesNotMatch(responseText, /InvalidStatusInternalFailure|secret-status-id/i);
|
||||
} finally {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not expose structured upstream error details while preserving classification", async () => {
|
||||
const executor = new LMArenaExecutor();
|
||||
__setTlsFetchOverrideForTesting(async () => ({
|
||||
@@ -749,6 +779,7 @@ describe("LMArena Executor", () => {
|
||||
assert.equal(errorLogs.length, 1);
|
||||
const responseText = await result.response.text();
|
||||
const json = JSON.parse(responseText);
|
||||
assert.equal(json.error?.message, "Arena upstream error");
|
||||
assert.equal(json.error?.type, "network_error");
|
||||
assert.equal(json.error?.code, "request_failed");
|
||||
const publicOutput = `${errorLogs.join("\n")}\n${responseText}`;
|
||||
@@ -866,6 +897,42 @@ describe("LMArena Executor", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not expose TLS-client failure details in the public response", async () => {
|
||||
const tlsFailure = new TlsClientUnavailableError(
|
||||
"NativeTlsBridgeFailure secret-adapter-id at /srv/private/lmarena-native.ts:22:4"
|
||||
) as TlsClientUnavailableError & { cause?: unknown };
|
||||
tlsFailure.cause = new Error("NativeTlsCause secret-cause-id");
|
||||
__setTlsFetchOverrideForTesting(async () => {
|
||||
throw tlsFailure;
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await new LMArenaExecutor().execute({
|
||||
model: TEST_ARENA_MODEL_ID,
|
||||
body: { messages: [{ role: "user", content: "Hello" }] },
|
||||
credentials: { cookie: "session=test" },
|
||||
signal: new AbortController().signal,
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 502);
|
||||
const responseText = await result.response.text();
|
||||
const json = JSON.parse(responseText);
|
||||
assert.deepEqual(json.error, {
|
||||
message:
|
||||
"Arena TLS impersonation unavailable: Arena upstream error. Install/repair tls-client-node native binary.",
|
||||
type: "upstream_error",
|
||||
code: "TLS_CLIENT_UNAVAILABLE",
|
||||
});
|
||||
assert.doesNotMatch(
|
||||
responseText,
|
||||
/NativeTlsBridgeFailure|secret-adapter-id|lmarena-native|NativeTlsCause|secret-cause-id/i
|
||||
);
|
||||
} finally {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a stable public fallback for blank network and upstream event errors", async (t) => {
|
||||
const stackOnly = "\n at SecretOnlyFrame (/srv/private/lmarena-stack-only.ts:2:3)";
|
||||
const cases = [
|
||||
@@ -918,6 +985,43 @@ describe("LMArena Executor", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not expose non-streaming upstream event details", async () => {
|
||||
const upstreamFailure =
|
||||
"ArenaEventInternalFailure secret-event-id\n" +
|
||||
" at ArenaEventAdapter (/srv/private/lmarena-event.ts:31:8)";
|
||||
__setTlsFetchOverrideForTesting(async () => ({
|
||||
status: 200,
|
||||
headers: new Headers({ "Content-Type": "text/event-stream" }),
|
||||
text: `3:${JSON.stringify(upstreamFailure)}\n`,
|
||||
body: null,
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await new LMArenaExecutor().execute({
|
||||
model: TEST_ARENA_MODEL_ID,
|
||||
body: { messages: [{ role: "user", content: "Hello" }] },
|
||||
credentials: { cookie: "session=test" },
|
||||
signal: new AbortController().signal,
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 502);
|
||||
const responseText = await result.response.text();
|
||||
const json = JSON.parse(responseText);
|
||||
assert.deepEqual(json.error, {
|
||||
message: "Arena upstream error",
|
||||
type: "api_error",
|
||||
code: "lmarena_error",
|
||||
});
|
||||
assert.doesNotMatch(
|
||||
responseText,
|
||||
/ArenaEventInternalFailure|secret-event-id|ArenaEventAdapter|lmarena-event/i
|
||||
);
|
||||
} finally {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a stable public fallback for blank streaming event errors", async () => {
|
||||
const stackOnly = "\n at SecretOnlyFrame (/srv/private/lmarena-stream-stack-only.ts:2:3)";
|
||||
const encoded = new TextEncoder().encode(`data: 3:${JSON.stringify(stackOnly)}\n\n`);
|
||||
@@ -950,13 +1054,118 @@ describe("LMArena Executor", () => {
|
||||
.filter((line) => line.startsWith("data: ") && line !== "data: [DONE]")
|
||||
.map((line) => JSON.parse(line.slice(6)))
|
||||
.find((chunk) => chunk.error);
|
||||
assert.equal(payload?.error?.message, "Arena upstream error");
|
||||
assert.deepEqual(payload?.error, {
|
||||
message: "Arena upstream error",
|
||||
type: "api_error",
|
||||
code: "lmarena_error",
|
||||
});
|
||||
assert.doesNotMatch(responseText, /SecretOnlyFrame|lmarena-stream-stack-only/);
|
||||
} finally {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not expose streaming upstream event details", async () => {
|
||||
const upstreamFailure =
|
||||
"StreamingArenaInternalFailure secret-stream-id\n" +
|
||||
" at StreamingArenaAdapter (/srv/private/lmarena-stream-event.ts:44:9)";
|
||||
const encoded = new TextEncoder().encode(`data: 3:${JSON.stringify(upstreamFailure)}\n\n`);
|
||||
__setTlsFetchOverrideForTesting(async () => ({
|
||||
status: 200,
|
||||
headers: new Headers({ "Content-Type": "text/event-stream" }),
|
||||
text: null,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoded);
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await new LMArenaExecutor().execute({
|
||||
model: TEST_ARENA_MODEL_ID,
|
||||
body: { messages: [{ role: "user", content: "Hello" }], stream: true },
|
||||
stream: true,
|
||||
credentials: { cookie: "session=test" },
|
||||
signal: new AbortController().signal,
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 200);
|
||||
const responseText = await result.response.text();
|
||||
const payload = responseText
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("data: ") && line !== "data: [DONE]")
|
||||
.map((line) => JSON.parse(line.slice(6)))
|
||||
.find((chunk) => chunk.error);
|
||||
assert.deepEqual(payload?.error, {
|
||||
message: "Arena upstream error",
|
||||
type: "api_error",
|
||||
code: "lmarena_error",
|
||||
});
|
||||
assert.doesNotMatch(
|
||||
responseText,
|
||||
/StreamingArenaInternalFailure|secret-stream-id|StreamingArenaAdapter|lmarena-stream-event/i
|
||||
);
|
||||
} finally {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not expose transport errors rejected by the upstream stream", async () => {
|
||||
const streamFailure = new Error(
|
||||
"ArenaStreamTransportFailure secret-transport-id at /srv/private/lmarena-reader.ts:52:6"
|
||||
) as Error & { cause?: unknown; statusCode?: number };
|
||||
streamFailure.cause = new Error("ArenaStreamCause secret-stream-cause-id");
|
||||
streamFailure.statusCode = 502;
|
||||
__setTlsFetchOverrideForTesting(async () => ({
|
||||
status: 200,
|
||||
headers: new Headers({ "Content-Type": "text/event-stream" }),
|
||||
text: null,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.error(streamFailure);
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await new LMArenaExecutor().execute({
|
||||
model: TEST_ARENA_MODEL_ID,
|
||||
body: { messages: [{ role: "user", content: "Hello" }], stream: true },
|
||||
stream: true,
|
||||
credentials: { cookie: "session=test" },
|
||||
signal: new AbortController().signal,
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 200);
|
||||
await assert.rejects(result.response.text(), (error: unknown) => {
|
||||
assert.ok(error instanceof Error);
|
||||
assert.equal(error.message, "Arena upstream stream error");
|
||||
assert.equal(error.stack, undefined);
|
||||
const projected = error as Error & {
|
||||
cause?: unknown;
|
||||
statusCode?: number;
|
||||
type?: string;
|
||||
code?: string;
|
||||
};
|
||||
assert.equal(projected.cause, undefined);
|
||||
assert.equal(projected.statusCode, 502);
|
||||
assert.equal(projected.type, "upstream_error");
|
||||
assert.equal(projected.code, "lmarena_stream_error");
|
||||
assert.doesNotMatch(
|
||||
error.message,
|
||||
/ArenaStreamTransportFailure|secret-transport-id|lmarena-reader|ArenaStreamCause|secret-stream-cause-id/i
|
||||
);
|
||||
return true;
|
||||
});
|
||||
} finally {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
}
|
||||
});
|
||||
|
||||
it("forwards optional browser reCAPTCHA token from credentials", () => {
|
||||
const executor = new LMArenaExecutor();
|
||||
const body = access(executor).transformRequest(
|
||||
|
||||
Reference in New Issue
Block a user