Skip to content

Commit 9331ac2

Browse files
committed
fix(scripts): cap issue labeler response bodies
1 parent 7f28c8b commit 9331ac2

2 files changed

Lines changed: 69 additions & 11 deletions

File tree

scripts/label-open-issues.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
44
import { dirname, join } from "node:path";
55
import { pathToFileURL } from "node:url";
66
import { isRecord } from "../src/utils.js";
7+
import { readBoundedResponseText as readBoundedBodyText } from "./lib/bounded-response.ts";
78
import { parseStrictIntegerOption } from "./lib/dev-tooling-safety.ts";
89

910
function writeStdoutLine(message = ""): void {
@@ -22,6 +23,7 @@ const WORK_BATCH_SIZE = 500;
2223
const STATE_VERSION = 1;
2324
const DEFAULT_OPENAI_TIMEOUT_MS = 60_000;
2425
const OPENAI_ERROR_BODY_MAX_CHARS = 4096;
26+
const OPENAI_RESPONSE_BODY_MAX_BYTES = 256 * 1024;
2527
const STATE_FILE_NAME = "issue-labeler-state.json";
2628
const CONFIG_BASE_DIR = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
2729
const STATE_FILE_PATH = join(CONFIG_BASE_DIR, "openclaw", STATE_FILE_NAME);
@@ -322,6 +324,19 @@ async function readBoundedResponseText(
322324
return truncated ? `${text}\n[truncated]` : text;
323325
}
324326

327+
async function readBoundedOpenAIJson(
328+
response: Response,
329+
maxBytes = OPENAI_RESPONSE_BODY_MAX_BYTES,
330+
): Promise<OpenAIResponse> {
331+
const text = await readBoundedBodyText(response, "OpenAI classification", maxBytes, {
332+
createTooLargeError: (message) =>
333+
Object.assign(new Error(message), {
334+
code: "ETOOBIG",
335+
}),
336+
});
337+
return JSON.parse(text) as OpenAIResponse;
338+
}
339+
325340
function logHeader(title: string) {
326341
writeStdoutLine(`\n${title}`);
327342
writeStdoutLine("=".repeat(title.length));
@@ -729,7 +744,7 @@ async function classifyItem(
729744
throw new Error(`OpenAI request failed (${response.status}): ${text}`);
730745
}
731746

732-
return (await response.json()) as OpenAIResponse;
747+
return await readBoundedOpenAIJson(response);
733748
},
734749
);
735750
const rawText = extractResponseText(payload);
@@ -993,6 +1008,7 @@ async function main() {
9931008
export const testing = {
9941009
classifyItem,
9951010
normalizeClassification,
1011+
readBoundedOpenAIJson,
9961012
readBoundedResponseText,
9971013
resolveOpenAITimeoutMs,
9981014
};

test/scripts/label-open-issues.test.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,16 @@ const labelItem = {
1010

1111
describe("label-open-issues helpers", () => {
1212
it("classifies items from OpenAI structured response text", async () => {
13-
const response = {
14-
ok: true,
15-
status: 200,
16-
json: async () => ({
13+
const response = new Response(
14+
JSON.stringify({
1715
output_text: JSON.stringify({
1816
category: "bug",
1917
isSupport: true,
2018
isSkillOnly: false,
2119
}),
2220
}),
23-
} as Response;
21+
{ status: 200 },
22+
);
2423

2524
await expect(
2625
testing.classifyItem(labelItem, "issue", {
@@ -55,11 +54,7 @@ describe("label-open-issues helpers", () => {
5554
});
5655

5756
it("times out stalled OpenAI classification body reads", async () => {
58-
const response = {
59-
ok: true,
60-
status: 200,
61-
json: () => new Promise(() => {}),
62-
} as Response;
57+
const response = new Response(new ReadableStream({}), { status: 200 });
6358
const request = testing.classifyItem(labelItem, "issue", {
6459
apiKey: "test-key",
6560
model: "test-model",
@@ -96,6 +91,53 @@ describe("label-open-issues helpers", () => {
9691
expect(message.length).toBeLessThan(4300);
9792
});
9893

94+
it("reads bounded OpenAI classification JSON responses", async () => {
95+
await expect(
96+
testing.readBoundedOpenAIJson(new Response('{"output_text":"{}"}'), 1024),
97+
).resolves.toEqual({ output_text: "{}" });
98+
});
99+
100+
it("rejects oversized OpenAI classification JSON responses by content length", async () => {
101+
let canceled = false;
102+
const response = new Response(
103+
new ReadableStream({
104+
cancel() {
105+
canceled = true;
106+
},
107+
}),
108+
{
109+
headers: {
110+
"content-length": "1025",
111+
},
112+
},
113+
);
114+
115+
await expect(testing.readBoundedOpenAIJson(response, 1024)).rejects.toMatchObject({
116+
code: "ETOOBIG",
117+
message: "OpenAI classification response body exceeded 1024 bytes",
118+
});
119+
expect(canceled).toBe(true);
120+
});
121+
122+
it("rejects oversized streamed OpenAI classification JSON responses", async () => {
123+
const encoder = new TextEncoder();
124+
const response = new Response(
125+
new ReadableStream({
126+
start(controller) {
127+
controller.enqueue(encoder.encode('{"output_text":"'));
128+
controller.enqueue(encoder.encode("x".repeat(1024)));
129+
controller.enqueue(encoder.encode('"}'));
130+
controller.close();
131+
},
132+
}),
133+
);
134+
135+
await expect(testing.readBoundedOpenAIJson(response, 1024)).rejects.toMatchObject({
136+
code: "ETOOBIG",
137+
message: "OpenAI classification response body exceeded 1024 bytes",
138+
});
139+
});
140+
99141
it("rejects invalid OpenAI classification timeout values", () => {
100142
expect(testing.resolveOpenAITimeoutMs("250")).toBe(250);
101143
expect(() => testing.resolveOpenAITimeoutMs("slow")).toThrow(

0 commit comments

Comments
 (0)