Skip to content

Commit 2e4733d

Browse files
committed
fix: preserve websocket turn cancellation hooks
1 parent e27f44c commit 2e4733d

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

src/ws-bridge.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,10 @@ export async function pumpResponsesSseToWebSocket(
140140
): Promise<void> {
141141
const reader = sseStream.getReader();
142142
const isCurrent = options.isCurrent ?? (() => true);
143-
ws.data.cancel = () => {
143+
const cancel = () => {
144144
void reader.cancel().catch(() => {});
145145
};
146+
ws.data.cancel = cancel;
146147

147148
const decoder = new TextDecoder();
148149
let buffer = "";
@@ -154,6 +155,8 @@ export async function pumpResponsesSseToWebSocket(
154155
const type = payloadType(payload);
155156
if (!type) {
156157
sendProtocolError(ws, 502, "Invalid JSON payload in upstream SSE frame");
158+
terminalSeen = true;
159+
void reader.cancel().catch(() => {});
157160
return true;
158161
}
159162
if (terminalSeen) return true;
@@ -191,7 +194,7 @@ export async function pumpResponsesSseToWebSocket(
191194
sendProtocolError(ws, 502, err instanceof Error ? err.message : String(err));
192195
}
193196
} finally {
194-
if (ws.data.cancel) ws.data.cancel = undefined;
197+
if (ws.data.cancel === cancel) ws.data.cancel = undefined;
195198
}
196199
}
197200

tests/ws-endpoint.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,34 @@ describe("WS endpoint re-framer (120/132)", () => {
131131
expect(sent).toEqual([]);
132132
});
133133

134+
test("stale pump cleanup does not erase the replacement turn cancel hook", async () => {
135+
const { ws } = mockWs();
136+
let current = false;
137+
const stalePump = pumpResponsesSseToWebSocket(ws, sseStream([
138+
'event: response.created\ndata: {"type":"response.created"}\n\n',
139+
]), { isCurrent: () => current });
140+
const replacementCancel = () => {};
141+
ws.data.cancel = replacementCancel;
142+
await stalePump;
143+
expect(ws.data.cancel).toBe(replacementCancel);
144+
});
145+
146+
test("invalid upstream SSE JSON emits one standalone protocol error and cancels", async () => {
147+
const { ws, sent } = mockWs();
148+
let cancelled = false;
149+
await pumpResponsesSseToWebSocket(ws, sseStream([
150+
"event: response.created\ndata: {not-json}\n\n",
151+
'event: response.completed\ndata: {"type":"response.completed"}\n\n',
152+
], () => { cancelled = true; }));
153+
expect(sent).toHaveLength(1);
154+
expect(JSON.parse(sent[0])).toMatchObject({
155+
type: "error",
156+
status: 502,
157+
error: { code: "websocket_protocol_error" },
158+
});
159+
expect(cancelled).toBe(true);
160+
});
161+
134162
test("converts successful Responses JSON into output_item.done plus response.completed frames", () => {
135163
const { ws, sent } = mockWs();
136164
sendResponsesJsonAsEvents(ws, {

0 commit comments

Comments
 (0)