Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 6 additions & 0 deletions src/providers/proxies/codex-websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ const isCompactionRequest = (
}
};

const ignoreSocketError = (): void => {};

const isSocketOpen = (socket: WebSocketLike): boolean =>
socket.readyState === undefined || socket.readyState === 1;

Expand Down Expand Up @@ -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);
Expand Down
37 changes: 37 additions & 0 deletions tests/providers/proxy-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, spyOn, test } from "bun:test";
import WebSocket from "ws";

import {
CLAUDE_SYSTEM_IDENTITY,
Expand Down Expand Up @@ -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);
Expand Down
Loading