mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-21 01:51:39 -06:00
fix(agents): bound provider JSON response reads (#95218)
This commit is contained in:
@@ -38,6 +38,32 @@ function createStreamingBinaryResponse(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function createStreamingJsonResponse(params: { chunkCount: number; chunkSize: number }): {
|
||||
response: Response;
|
||||
getReadCount: () => number;
|
||||
} {
|
||||
// Streaming fixture proves oversized JSON reads stop before buffering everything.
|
||||
let reads = 0;
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (reads >= params.chunkCount) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
reads += 1;
|
||||
controller.enqueue(encoder.encode("a".repeat(params.chunkSize)));
|
||||
},
|
||||
});
|
||||
return {
|
||||
response: new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
getReadCount: () => reads,
|
||||
};
|
||||
}
|
||||
|
||||
describe("provider error utils", () => {
|
||||
it("formats nested provider error details with request ids", async () => {
|
||||
const response = new Response(
|
||||
@@ -211,6 +237,32 @@ describe("provider error utils", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("parses well-formed JSON responses under the byte cap", async () => {
|
||||
const response = new Response(JSON.stringify({ models: ["a", "b"] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
await expect(
|
||||
readProviderJsonResponse<{ models: string[] }>(response, "Provider catalog failed"),
|
||||
).resolves.toEqual({ models: ["a", "b"] });
|
||||
});
|
||||
|
||||
it("caps successful JSON responses instead of buffering oversized bodies", async () => {
|
||||
const streamed = createStreamingJsonResponse({
|
||||
chunkCount: 20,
|
||||
chunkSize: 1024,
|
||||
});
|
||||
|
||||
await expect(
|
||||
readProviderJsonResponse(streamed.response, "Provider catalog failed", {
|
||||
maxBytes: 2048,
|
||||
}),
|
||||
).rejects.toThrow("Provider catalog failed: JSON response exceeds 2048 bytes");
|
||||
|
||||
expect(streamed.getReadCount()).toBeLessThan(20);
|
||||
});
|
||||
|
||||
it("caps successful binary responses instead of buffering oversized bodies", async () => {
|
||||
const streamed = createStreamingBinaryResponse({
|
||||
chunkCount: 20,
|
||||
|
||||
@@ -13,6 +13,7 @@ export { normalizeOptionalString as trimToUndefined } from "../../packages/norma
|
||||
|
||||
const ERROR_BODY_METADATA_LIMIT = 500;
|
||||
const PROVIDER_BINARY_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
|
||||
const PROVIDER_JSON_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
/** Returns a plain object view for provider JSON payloads when one exists. */
|
||||
export function asObject(value: unknown): Record<string, unknown> | undefined {
|
||||
@@ -287,10 +288,24 @@ export async function assertOkOrThrowHttpError(response: Response, label: string
|
||||
throw await createProviderHttpError(response, label, { statusPrefix: "HTTP " });
|
||||
}
|
||||
|
||||
/** Parses a provider JSON response and wraps malformed JSON with the caller's label. */
|
||||
export async function readProviderJsonResponse<T>(response: Response, label: string): Promise<T> {
|
||||
/**
|
||||
* Parses a provider JSON response under a byte cap and wraps malformed JSON with the caller's label.
|
||||
*
|
||||
* The body is read through the same bounded reader as binary responses so a provider that streams an
|
||||
* unbounded JSON body cannot force the runtime to buffer the whole payload before parsing.
|
||||
*/
|
||||
export async function readProviderJsonResponse<T>(
|
||||
response: Response,
|
||||
label: string,
|
||||
opts?: { maxBytes?: number },
|
||||
): Promise<T> {
|
||||
const maxBytes = opts?.maxBytes ?? PROVIDER_JSON_RESPONSE_MAX_BYTES;
|
||||
const bytes = await readResponseWithLimit(response, maxBytes, {
|
||||
onOverflow: ({ maxBytes: maxBytesLocal }) =>
|
||||
new Error(`${label}: JSON response exceeds ${maxBytesLocal} bytes`),
|
||||
});
|
||||
try {
|
||||
return (await response.json()) as T;
|
||||
return JSON.parse(new TextDecoder().decode(bytes)) as T;
|
||||
} catch (cause) {
|
||||
throw new Error(`${label}: malformed JSON response`, { cause });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user