fix(provider-usage): bound usage response body reads

This commit is contained in:
Vincent Koc
2026-06-28 21:57:22 -07:00
committed by GitHub
parent 46e119074e
commit db2786bde1
5 changed files with 88 additions and 10 deletions
@@ -49,6 +49,33 @@ function makeOrgAResponse() {
return makeResponse(200, [{ uuid: "org-a" }]);
}
function makeOversizedJsonResponse(status: number): {
response: Response;
state: { canceled: boolean; enqueuedBytes: number };
} {
const state = { canceled: false, enqueuedBytes: 0 };
const chunkSize = 1024 * 1024;
let emitted = 0;
const response = new Response(
new ReadableStream({
pull(controller) {
if (emitted >= 64) {
controller.close();
return;
}
emitted += 1;
state.enqueuedBytes += chunkSize;
controller.enqueue(new Uint8Array(chunkSize));
},
cancel() {
state.canceled = true;
},
}),
{ status, headers: { "Content-Type": "application/json" } },
);
return { response, state };
}
describe("fetchClaudeUsage", () => {
afterEach(() => {
vi.unstubAllEnvs();
@@ -129,6 +156,18 @@ describe("fetchClaudeUsage", () => {
expect(result.windows).toHaveLength(0);
});
it("bounds oversized oauth error bodies and cancels the stream", async () => {
const oversized = makeOversizedJsonResponse(403);
const mockFetch = createProviderUsageFetch(async () => oversized.response);
const result = await fetchClaudeUsage("token", 5000, mockFetch);
expect(result.error).toBe("HTTP 403");
expect(result.windows).toHaveLength(0);
expect(oversized.state.canceled).toBe(true);
expect(oversized.state.enqueuedBytes).toBeLessThan(64 * 1024 * 1024);
});
it("returns a stable error for malformed successful oauth usage JSON", async () => {
const mockFetch = createProviderUsageFetch(async () => makeResponse(200, "{not json"));
+1 -1
View File
@@ -1,4 +1,5 @@
// Fetches Claude provider usage windows.
import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
import {
buildUsageHttpErrorSnapshot,
discardUsageResponseBody,
@@ -7,7 +8,6 @@ import {
} from "./provider-usage.fetch.shared.js";
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
type ClaudeUsageResponse = {
five_hour?: { utilization?: number; resets_at?: string };
+41 -7
View File
@@ -22,6 +22,33 @@ async function expectMinimaxUsageResult(params: {
expect(result.windows).toEqual(params.expected.windows);
}
function makeOversizedJsonResponse(): {
response: Response;
state: { canceled: boolean; enqueuedBytes: number };
} {
const state = { canceled: false, enqueuedBytes: 0 };
const chunkSize = 1024 * 1024;
let emitted = 0;
const response = new Response(
new ReadableStream({
pull(controller) {
if (emitted >= 64) {
controller.close();
return;
}
emitted += 1;
state.enqueuedBytes += chunkSize;
controller.enqueue(new Uint8Array(chunkSize));
},
cancel() {
state.canceled = true;
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
return { response, state };
}
describe("fetchMinimaxUsage", () => {
it.each([
{
@@ -99,6 +126,18 @@ describe("fetchMinimaxUsage", () => {
expect(result.windows).toHaveLength(0);
});
it("bounds oversized successful JSON responses and cancels the stream", async () => {
const oversized = makeOversizedJsonResponse();
const mockFetch = createProviderUsageFetch(async () => oversized.response);
const result = await fetchMinimaxUsage("key", 5000, mockFetch);
expect(result.error).toBe("Invalid JSON");
expect(result.windows).toHaveLength(0);
expect(oversized.state.canceled).toBe(true);
expect(oversized.state.enqueuedBytes).toBeLessThan(64 * 1024 * 1024);
});
it.each([
{
name: "derives usage from used/total fields and includes reset + plan",
@@ -293,13 +332,8 @@ describe("fetchMinimaxUsage", () => {
first: sharedUsage,
nested: [sharedUsage],
};
const mockFetch = createProviderUsageFetch(
async () =>
({
ok: true,
status: 200,
json: async () => ({ data: dataWithSharedReference }),
}) as Response,
const mockFetch = createProviderUsageFetch(async () =>
makeResponse(200, { data: dataWithSharedReference }),
);
const result = await fetchMinimaxUsage("key", 5000, mockFetch);
+4 -1
View File
@@ -1,6 +1,7 @@
// Fetches and normalizes MiniMax provider usage records.
import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
import { isRecord } from "../utils.js";
import {
buildUsageHttpErrorSnapshot,
@@ -418,7 +419,9 @@ export async function fetchMinimaxUsage(
});
}
const data = (await res.json().catch(() => null)) as MinimaxUsageResponse;
const data = await readProviderJsonResponse<MinimaxUsageResponse>(res, "minimax usage").catch(
() => null,
);
if (!isRecord(data)) {
return {
provider: "minimax",
+3 -1
View File
@@ -1,5 +1,6 @@
// Shared fetch and parsing helpers for provider usage endpoints.
import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
import { parseFiniteNumber as parseFiniteNumberish } from "./parse-finite-number.js";
import { PROVIDER_LABELS } from "./provider-usage.shared.js";
import type { ProviderUsageSnapshot, UsageProviderId } from "./provider-usage.types.js";
@@ -67,7 +68,8 @@ export async function readUsageJson(
response: Response,
): Promise<{ ok: true; data: unknown } | { ok: false; snapshot: ProviderUsageSnapshot }> {
try {
return { ok: true, data: await response.json() };
const data = await readProviderJsonResponse<unknown>(response, `${provider} usage`);
return { ok: true, data };
} catch {
return {
ok: false,