mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 11:02:26 +00:00
fix(tools): release bounded web response readers
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Shared web helper tests cover timeout normalization and process-local cache
|
||||
// expiry guards.
|
||||
// Shared web helper tests cover timeout normalization, process-local cache
|
||||
// expiry guards, and bounded response body cleanup.
|
||||
import {
|
||||
MAX_TIMER_TIMEOUT_MS,
|
||||
MAX_TIMER_TIMEOUT_SECONDS,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
readCache,
|
||||
readResponseText,
|
||||
resolvePositiveTimeoutSeconds,
|
||||
resolveTimeoutSeconds,
|
||||
withTimeout,
|
||||
@@ -18,6 +19,28 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function responseFromReader(params: {
|
||||
chunks: string[];
|
||||
cancel: () => Promise<void>;
|
||||
releaseLock: () => void;
|
||||
}): Response {
|
||||
const chunks: Array<ReadableStreamReadResult<Uint8Array>> = params.chunks.map((chunk) => ({
|
||||
done: false,
|
||||
value: new TextEncoder().encode(chunk),
|
||||
}));
|
||||
chunks.push({ done: true, value: undefined });
|
||||
const reader = {
|
||||
read: async () => chunks.shift() ?? { done: true, value: undefined },
|
||||
cancel: params.cancel,
|
||||
releaseLock: params.releaseLock,
|
||||
} as ReadableStreamDefaultReader<Uint8Array>;
|
||||
|
||||
return {
|
||||
body: { getReader: () => reader },
|
||||
headers: new Headers({ "content-type": "text/plain; charset=utf-8" }),
|
||||
} as Response;
|
||||
}
|
||||
|
||||
describe("web shared timeout seconds", () => {
|
||||
it("caps timeoutSeconds at the shared timer-safe ceiling", () => {
|
||||
expect(resolveTimeoutSeconds(Number.MAX_SAFE_INTEGER, 30)).toBe(MAX_TIMER_TIMEOUT_SECONDS);
|
||||
@@ -86,3 +109,41 @@ describe("web shared withTimeout", () => {
|
||||
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readResponseText", () => {
|
||||
it("releases bounded response readers after complete reads", async () => {
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const releaseLock = vi.fn();
|
||||
const response = responseFromReader({
|
||||
chunks: ["hello", " world"],
|
||||
cancel,
|
||||
releaseLock,
|
||||
});
|
||||
|
||||
await expect(readResponseText(response, { maxBytes: 64 })).resolves.toEqual({
|
||||
text: "hello world",
|
||||
truncated: false,
|
||||
bytesRead: 11,
|
||||
});
|
||||
expect(cancel).not.toHaveBeenCalled();
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("cancels and releases bounded response readers after truncation", async () => {
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const releaseLock = vi.fn();
|
||||
const response = responseFromReader({
|
||||
chunks: ["hello world"],
|
||||
cancel,
|
||||
releaseLock,
|
||||
});
|
||||
|
||||
await expect(readResponseText(response, { maxBytes: 5 })).resolves.toEqual({
|
||||
text: "hello",
|
||||
truncated: true,
|
||||
bytesRead: 5,
|
||||
});
|
||||
expect(cancel).toHaveBeenCalledTimes(1);
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -285,6 +285,9 @@ export async function readResponseText(
|
||||
// let cleanup turn a bounded read into a hung fetch.
|
||||
void reader.cancel().catch(() => undefined);
|
||||
}
|
||||
try {
|
||||
reader.releaseLock();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const bytes = concatBytes(parts, bytesRead);
|
||||
|
||||
Reference in New Issue
Block a user