fix(e2e): bound ClawHub preflight response bodies

This commit is contained in:
Vincent Koc
2026-05-28 21:19:31 +02:00
parent 396a8ef6f8
commit 4d5b317ace
2 changed files with 97 additions and 3 deletions

View File

@@ -8,6 +8,10 @@ const CLAWHUB_PREFLIGHT_TIMEOUT_MS = readPositiveInt(
process.env.OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_TIMEOUT_MS,
30_000,
);
const CLAWHUB_PREFLIGHT_BODY_MAX_BYTES = readPositiveInt(
process.env.OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_BODY_MAX_BYTES,
1024 * 1024,
);
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
const scratchFile = (name) => path.join(scratchRoot, name);
@@ -42,6 +46,47 @@ async function withTimeout(label, timeoutMs, run) {
}
}
function bodyTooLargeError(label, byteLimit) {
return Object.assign(new Error(`${label} response body exceeded ${byteLimit} bytes`), {
code: "ETOOBIG",
});
}
async function readBoundedResponseText(response, label, byteLimit) {
const contentLength = response.headers.get("content-length");
if (contentLength) {
const parsedLength = Number(contentLength);
if (Number.isSafeInteger(parsedLength) && parsedLength > byteLimit) {
await response.body?.cancel().catch(() => {});
throw bodyTooLargeError(label, byteLimit);
}
}
if (!response.body) {
return "";
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let byteCount = 0;
let text = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return text + decoder.decode();
}
byteCount += value.byteLength;
if (byteCount > byteLimit) {
await reader.cancel().catch(() => {});
throw bodyTooLargeError(label, byteLimit);
}
text += decoder.decode(value, { stream: true });
}
} finally {
reader.releaseLock();
}
}
function resolveHomePath(value) {
if (value === "~") {
return process.env.HOME;
@@ -806,16 +851,31 @@ async function assertClawHubPreflight() {
const body = await withTimeout(
`ClawHub package preflight response for ${packageName}`,
CLAWHUB_PREFLIGHT_TIMEOUT_MS,
() => response.text().catch(() => ""),
() =>
readBoundedResponseText(
response,
`ClawHub package preflight response for ${packageName}`,
CLAWHUB_PREFLIGHT_BODY_MAX_BYTES,
),
);
throw new Error(
`ClawHub package preflight failed for ${packageName}: ${response.status} ${body}`,
);
}
const detail = await withTimeout(
const rawDetail = await withTimeout(
`ClawHub package preflight response for ${packageName}`,
CLAWHUB_PREFLIGHT_TIMEOUT_MS,
() => response.json(),
() =>
readBoundedResponseText(
response,
`ClawHub package preflight response for ${packageName}`,
CLAWHUB_PREFLIGHT_BODY_MAX_BYTES,
),
);
const detail = await withTimeout(
`ClawHub package preflight JSON for ${packageName}`,
CLAWHUB_PREFLIGHT_TIMEOUT_MS,
() => JSON.parse(rawDetail),
);
const family = detail.package?.family;
if (family !== "code-plugin" && family !== "bundle-plugin") {

View File

@@ -236,4 +236,38 @@ describe("plugins Docker assertions", () => {
});
}
});
it("bounds ClawHub package metadata response bodies", async () => {
const server = createServer((_request, response) => {
response.writeHead(500, { "content-type": "text/plain" });
response.end("x".repeat(128));
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
try {
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("expected TCP server address");
}
const result = await runAssertionAsync(["clawhub-preflight"], {
CLAWHUB_PLUGIN_ID: "openclaw-kitchen-sink-fixture",
CLAWHUB_PLUGIN_SPEC: "clawhub:@openclaw/kitchen-sink",
OPENCLAW_CLAWHUB_URL: `http://127.0.0.1:${address.port}`,
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_BODY_MAX_BYTES: "16",
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_TIMEOUT_MS: "1000",
});
expect(result.status).not.toBe(0);
expect(result.stderr).toContain(
"ClawHub package preflight response for @openclaw/kitchen-sink response body exceeded 16 bytes",
);
expect(result.stderr).not.toContain("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
} finally {
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});
});