mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(clawrouter): show stable error for malformed usage responses (#119580)
* fix(clawrouter): handle malformed usage responses * fix(clawrouter): use canonical usage record coercion Punchcard-Session: clear-orchard-lantern-bc --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -222,20 +222,61 @@ describe("ClawRouter usage", () => {
|
||||
expect(snapshot.summary).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects usage JSON containing invalid UTF-8", async () => {
|
||||
it.each([
|
||||
["malformed JSON", new TextEncoder().encode('{"budget":')],
|
||||
["a non-object JSON root", new TextEncoder().encode("null")],
|
||||
])("reports %s as a malformed usage response", async (_label, body) => {
|
||||
const snapshot = await fetchClawRouterUsage({
|
||||
token: "test-token",
|
||||
timeoutMs: 5000,
|
||||
fetchGuard: mockFetchGuard(new Response(body)),
|
||||
});
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
provider: "clawrouter",
|
||||
displayName: "ClawRouter",
|
||||
windows: [],
|
||||
error: "Malformed usage response",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports invalid UTF-8 as a malformed usage response", async () => {
|
||||
const prefix = new TextEncoder().encode(
|
||||
'{"budget":{"configured":true,"windowKey":"default/test-policy/2026-',
|
||||
);
|
||||
const suffix = new TextEncoder().encode('","limitMicros":1000000,"spentMicros":500000}}');
|
||||
const body = new Uint8Array([...prefix, 0xff, ...suffix]);
|
||||
|
||||
const snapshot = await fetchClawRouterUsage({
|
||||
token: "test-token",
|
||||
timeoutMs: 5000,
|
||||
fetchGuard: mockFetchGuard(new Response(body)),
|
||||
});
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
provider: "clawrouter",
|
||||
displayName: "ClawRouter",
|
||||
windows: [],
|
||||
error: "Malformed usage response",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves response stream failures as transport errors", async () => {
|
||||
const response = new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.error(new TypeError("usage stream failed"));
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
fetchClawRouterUsage({
|
||||
token: "test-token",
|
||||
timeoutMs: 5000,
|
||||
fetchGuard: mockFetchGuard(new Response(body)),
|
||||
fetchGuard: mockFetchGuard(response),
|
||||
}),
|
||||
).rejects.toThrow(TypeError);
|
||||
).rejects.toThrow("usage stream failed");
|
||||
});
|
||||
|
||||
it("cancels non-OK usage response body before throwing", async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
fetchWithSsrFGuard,
|
||||
ssrfPolicyFromHttpBaseUrlAllowedHostname,
|
||||
} from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { asFiniteNumberInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { asFiniteNumberInRange, asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { normalizeClawRouterRootUrl } from "./provider-catalog.js";
|
||||
|
||||
const CLAWROUTER_USAGE_RESPONSE_MAX_BYTES = 1024 * 1024;
|
||||
@@ -72,16 +72,19 @@ function buildSummary(payload: ClawRouterUsagePayload): string | undefined {
|
||||
async function readClawRouterUsagePayload(
|
||||
response: Response,
|
||||
timeoutMs: number,
|
||||
): Promise<ClawRouterUsagePayload> {
|
||||
): Promise<ClawRouterUsagePayload | undefined> {
|
||||
const buffer = await readResponseWithLimit(response, CLAWROUTER_USAGE_RESPONSE_MAX_BYTES, {
|
||||
chunkTimeoutMs: timeoutMs,
|
||||
onOverflow: ({ maxBytes }) => new Error(`ClawRouter usage response exceeds ${maxBytes} bytes`),
|
||||
onIdleTimeout: ({ chunkTimeoutMs }) =>
|
||||
new Error(`ClawRouter usage response stalled: no data received for ${chunkTimeoutMs}ms`),
|
||||
});
|
||||
return JSON.parse(
|
||||
new TextDecoder("utf-8", { fatal: true }).decode(buffer),
|
||||
) as ClawRouterUsagePayload;
|
||||
try {
|
||||
const payload: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer));
|
||||
return asOptionalRecord(payload) as ClawRouterUsagePayload | undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchClawRouterUsage(params: {
|
||||
@@ -116,6 +119,14 @@ export async function fetchClawRouterUsage(params: {
|
||||
throw new Error(`ClawRouter usage request failed (HTTP ${response.status})`);
|
||||
}
|
||||
const payload = await readClawRouterUsagePayload(response, params.timeoutMs);
|
||||
if (!payload) {
|
||||
return {
|
||||
provider: "clawrouter",
|
||||
displayName: "ClawRouter",
|
||||
windows: [],
|
||||
error: "Malformed usage response",
|
||||
};
|
||||
}
|
||||
const budget = payload.budget;
|
||||
const limitMicros = asFiniteNumberInRange(budget?.limitMicros, { min: 0 });
|
||||
const spentMicros = asFiniteNumberInRange(budget?.spentMicros, { min: 0 });
|
||||
|
||||
Reference in New Issue
Block a user