mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(web-fetch): report exact-limit response bodies as complete, not truncated [AI-assisted] (#102389)
* fix(web-fetch): report exact-limit response bodies as complete, not truncated * fix(web-fetch): skip zero-byte chunks when confirming overflow past the byte cap * fix(web-fetch): keep uncertain capped bodies truncated --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -23,14 +23,26 @@ function responseFromReader(params: {
|
||||
chunks: string[];
|
||||
cancel: () => Promise<void>;
|
||||
releaseLock: () => void;
|
||||
readError?: Error;
|
||||
}): Response {
|
||||
const chunks: Array<ReadableStreamReadResult<Uint8Array>> = params.chunks.map((chunk) => ({
|
||||
done: false,
|
||||
value: new TextEncoder().encode(chunk),
|
||||
}));
|
||||
chunks.push({ done: true, value: undefined });
|
||||
if (!params.readError) {
|
||||
chunks.push({ done: true, value: undefined });
|
||||
}
|
||||
const reader = {
|
||||
read: async () => chunks.shift() ?? { done: true, value: undefined },
|
||||
read: async () => {
|
||||
const next = chunks.shift();
|
||||
if (next) {
|
||||
return next;
|
||||
}
|
||||
if (params.readError) {
|
||||
throw params.readError;
|
||||
}
|
||||
return { done: true, value: undefined };
|
||||
},
|
||||
cancel: params.cancel,
|
||||
releaseLock: params.releaseLock,
|
||||
} as ReadableStreamDefaultReader<Uint8Array>;
|
||||
@@ -147,6 +159,97 @@ describe("readResponseText", () => {
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not mark exact-limit streamed responses as truncated", async () => {
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const releaseLock = vi.fn();
|
||||
const response = responseFromReader({
|
||||
chunks: ["hello"],
|
||||
cancel,
|
||||
releaseLock,
|
||||
});
|
||||
|
||||
await expect(readResponseText(response, { maxBytes: 5 })).resolves.toEqual({
|
||||
text: "hello",
|
||||
truncated: false,
|
||||
bytesRead: 5,
|
||||
});
|
||||
expect(cancel).not.toHaveBeenCalled();
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not mark multi-chunk exact-limit streamed responses as truncated", async () => {
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const releaseLock = vi.fn();
|
||||
const response = responseFromReader({
|
||||
chunks: ["hel", "lo"],
|
||||
cancel,
|
||||
releaseLock,
|
||||
});
|
||||
|
||||
await expect(readResponseText(response, { maxBytes: 5 })).resolves.toEqual({
|
||||
text: "hello",
|
||||
truncated: false,
|
||||
bytesRead: 5,
|
||||
});
|
||||
expect(cancel).not.toHaveBeenCalled();
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("marks responses that exceed the limit as truncated after confirming overflow", async () => {
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const releaseLock = vi.fn();
|
||||
const response = responseFromReader({
|
||||
chunks: ["hello", "!"],
|
||||
cancel,
|
||||
releaseLock,
|
||||
});
|
||||
|
||||
await expect(readResponseText(response, { maxBytes: 5 })).resolves.toEqual({
|
||||
text: "hello",
|
||||
truncated: true,
|
||||
bytesRead: 5,
|
||||
});
|
||||
expect(cancel).toHaveBeenCalledTimes(1);
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not mark exact-limit responses as truncated when followed by zero-byte chunks", async () => {
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const releaseLock = vi.fn();
|
||||
const response = responseFromReader({
|
||||
chunks: ["hello", ""],
|
||||
cancel,
|
||||
releaseLock,
|
||||
});
|
||||
|
||||
await expect(readResponseText(response, { maxBytes: 5 })).resolves.toEqual({
|
||||
text: "hello",
|
||||
truncated: false,
|
||||
bytesRead: 5,
|
||||
});
|
||||
expect(cancel).not.toHaveBeenCalled();
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps exact-limit responses truncated when the confirming read fails", async () => {
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const releaseLock = vi.fn();
|
||||
const response = responseFromReader({
|
||||
chunks: ["hello"],
|
||||
cancel,
|
||||
releaseLock,
|
||||
readError: new Error("stream failed before EOF"),
|
||||
});
|
||||
|
||||
await expect(readResponseText(response, { maxBytes: 5 })).resolves.toEqual({
|
||||
text: "hello",
|
||||
truncated: true,
|
||||
bytesRead: 5,
|
||||
});
|
||||
expect(cancel).toHaveBeenCalledTimes(1);
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not invoke whole-body fallbacks when maxBytes is set", async () => {
|
||||
const arrayBuffer = vi.fn(async () => new TextEncoder().encode("hello").buffer);
|
||||
const text = vi.fn(async () => "hello");
|
||||
|
||||
@@ -272,8 +272,25 @@ export async function readResponseText(
|
||||
bytesRead += chunk.byteLength;
|
||||
parts.push(chunk);
|
||||
|
||||
if (truncated || bytesRead >= maxBytes) {
|
||||
if (truncated) {
|
||||
break;
|
||||
}
|
||||
if (bytesRead >= maxBytes) {
|
||||
// Reached the byte cap. A body that is exactly maxBytes bytes is
|
||||
// complete only once EOF confirms it. Keep the conservative result
|
||||
// if that confirming read fails or the body continues.
|
||||
truncated = true;
|
||||
while (true) {
|
||||
const { done: atEnd, value: extra } = await reader.read();
|
||||
if (atEnd) {
|
||||
truncated = false;
|
||||
break;
|
||||
}
|
||||
if (extra && extra.byteLength > 0) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user