mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(provider-usage): bound usage response body reads
This commit is contained in:
@@ -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,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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user