From 28b5f90b4a6cbb86892dc2be0cf089a2ff78cd82 Mon Sep 17 00:00:00 2001 From: rexdotsh <65942753+rexdotsh@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:51:09 +0000 Subject: [PATCH 1/2] fix(codex): keep an error listener on relay WebSockets Bun 1.4.4's built-in ws throws an "error" event that has no listener, like npm ws on Node. The relay detaches its listeners before terminating a socket mid-handshake (request abort or connect timeout) and while a socket sits idle in the session cache, so either path crashed the process. --- src/providers/proxies/codex-websocket.ts | 6 ++++ tests/providers/proxy-contract.test.ts | 37 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/providers/proxies/codex-websocket.ts b/src/providers/proxies/codex-websocket.ts index c819ed4..72cee49 100644 --- a/src/providers/proxies/codex-websocket.ts +++ b/src/providers/proxies/codex-websocket.ts @@ -148,6 +148,8 @@ const isCompactionRequest = ( } }; +const ignoreSocketError = (): void => {}; + const isSocketOpen = (socket: WebSocketLike): boolean => socket.readyState === undefined || socket.readyState === 1; @@ -309,6 +311,10 @@ const connectWebSocket = ( return; } + // ws throws when it emits "error" with no listener, and the relay detaches + // its own listeners before terminating a socket or caching it while idle. + socket.addEventListener("error", ignoreSocketError); + socket.addEventListener("open", onOpen); socket.addEventListener("error", onError); socket.addEventListener("close", onClose); diff --git a/tests/providers/proxy-contract.test.ts b/tests/providers/proxy-contract.test.ts index 14fa463..32fe7d5 100644 --- a/tests/providers/proxy-contract.test.ts +++ b/tests/providers/proxy-contract.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, spyOn, test } from "bun:test"; +import WebSocket from "ws"; import { CLAUDE_SYSTEM_IDENTITY, @@ -1389,6 +1390,42 @@ describe("proxy contract: codex", () => { expect(warnings).toHaveLength(0); }); + test("aborting during the websocket handshake does not crash the process", async () => { + // Accepts the TCP connection but never answers the upgrade request. + const upstream = Bun.listen({ + hostname: "127.0.0.1", + port: 0, + socket: { data(): void {} }, + }); + class LocalWebSocket extends WebSocket { + constructor(_url: string, options?: MockWebSocketOptions) { + super(`ws://127.0.0.1:${upstream.port}/`, options); + } + } + setCodexWebSocketConstructorForTests(LocalWebSocket); + const abortController = new AbortController(); + + try { + const responsePromise = tryProxyCodexWebSocket({ + headers: new Headers({ authorization: "Bearer codex-access" }), + bodyJson: { + model: "gpt-5.5", + stream: true, + input: [{ role: "user", content: "hello" }], + }, + accountKey: "key-1:account-1", + signal: abortController.signal, + }); + await Bun.sleep(50); + abortController.abort(new Error("stop")); + expect(await responsePromise).toBeNull(); + // ws emits the handshake abort error on a later tick. + await Bun.sleep(50); + } finally { + upstream.stop(true); + } + }); + test("websocket reasoning diagnostics observe upstream events without rewriting them", async () => { const sentBodies: unknown[] = []; const sockets = installManualCodexWebSocketMock(sentBodies); From ef25afc693424110de7b239de62eda5f32dfb3d0 Mon Sep 17 00:00:00 2001 From: rexdotsh <65942753+rexdotsh@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:51:11 +0000 Subject: [PATCH 2/2] chore: upgrade Bun to 1.4.4 --- Dockerfile | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4f9f4d8..707e726 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -FROM oven/bun:1.4.3 AS base +FROM oven/bun:1.4.4 AS base WORKDIR /usr/src/app FROM base AS prod-deps diff --git a/package.json b/package.json index 12b62bc..2d3e208 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "kleis", "type": "module", - "packageManager": "bun@1.4.3", + "packageManager": "bun@1.4.4", "scripts": { "dev": "bun run --hot --no-clear-screen src/index.ts", "build": "bun build src/index.ts --target=bun --outdir=dist --packages=external",