Skip to content

Commit fe53d58

Browse files
committed
fix(streaming): taint malformed done events
1 parent 72e9ca3 commit fe53d58

2 files changed

Lines changed: 30 additions & 0 deletions

File tree

‎src/server/responses-snapshot-repair.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ export function createResponsesSnapshotPayloadRewrite(
213213
let nextEvent = event;
214214
let changed = false;
215215

216+
if (type === "response.output_item.done" && !isPlainObject(event.item)) {
217+
reconstructionTainted = true;
218+
}
219+
216220
if ((type === "response.output_item.added" || type === "response.output_item.done")
217221
&& isPlainObject(event.item)) {
218222
const itemStatus = type === "response.output_item.done" ? "completed" : "in_progress";

‎tests/responses-snapshot-repair.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,32 @@ describe("Responses sparse-snapshot repair", () => {
237237
expect(terminal.response.output).toEqual([]);
238238
});
239239

240+
test("suppresses reconstruction after malformed done events", () => {
241+
const malformedDoneEvents = [
242+
{ type: "response.output_item.done", output_index: 1 },
243+
{ type: "response.output_item.done", output_index: 1, item: "invalid" },
244+
{ type: "response.output_item.done", output_index: "1", item: { type: "message" } },
245+
{ type: "response.output_item.done", output_index: 1, item: { id: "msg_1" } },
246+
];
247+
248+
for (const malformedDoneEvent of malformedDoneEvents) {
249+
const rewrite = createResponsesSnapshotPayloadRewrite();
250+
rewrite(JSON.stringify({
251+
type: "response.output_item.done",
252+
output_index: 0,
253+
item: { id: "msg_0", type: "message" },
254+
}));
255+
rewrite(JSON.stringify(malformedDoneEvent));
256+
257+
const terminal = JSON.parse(rewrite(JSON.stringify({
258+
type: "response.completed",
259+
response: { id: "resp_malformed_done", object: "response" },
260+
}))) as { response: { output?: unknown } };
261+
262+
expect(terminal.response.output).toEqual([]);
263+
}
264+
});
265+
240266
test("suppresses partial reconstruction after retained-item bounds are exceeded", () => {
241267
const rewrite = createResponsesSnapshotPayloadRewrite();
242268
for (let outputIndex = 0; outputIndex <= MAX_COMPLETED_OUTPUT_ITEMS; outputIndex += 1) {

0 commit comments

Comments
 (0)