Skip to content

fix: answer WebSocket upgrades with a clean 426 so Codex falls back to HTTP immediately (#2) - #169

Merged
ranxianglei merged 3 commits into
masterfrom
2026-08-17_ws-upgrade-426
Aug 21, 2026
Merged

ranxianglei merged 3 commits into
masterfrom
2026-08-17_ws-upgrade-426

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Problem

Fixes #2 (codex 版本可能因为默认 socket 模式卡顿, 十几秒自动切 http 恢复).

New Codex versions default to Responses-over-WebSocket (supports_websockets=true). In codex source (codex-rs/core/src/client.rs), only an HTTP 426 on the WS handshake triggers immediate fallback to HTTP POST; any other failure (e.g. socket destroyed with no response) goes through retry/backoff budgets first — that's the 10-20s stall users see before it auto-recovers on HTTP.

bili already wrote a 426 inside its request handler, but the HTTP server never registered an 'upgrade' event listener, so Node's handling of upgrade requests is version-dependent — often the socket is destroyed without any response, pushing Codex onto the slow path.

Fix

  • src/server.ts: explicit server.on("upgrade") handler — always answers 426 Upgrade Required + Connection: close with a JSON error body and closes the socket immediately. Consistent on every Node version; Codex receiving 426 switches to HTTP POST in the same turn.
  • Also covers MITM-decrypted TLS connections (bili codex mode: chatgpt.com / api.openai.com), since those land on the same server object.

Tests

  • New tests/ws-upgrade.test.ts: 426 + immediate close verified on both the plain path and the full CONNECT → TLS → upgrade MITM path.
  • npm run typecheck ✓, full suite 455/455 ✓, npm run build ✓.

Note: src/update.ts untouched — no no-op validation release needed; standard §5 release flow after merge.

Sisyphus added 3 commits August 17, 2026 11:52
…client abort

- The handle() `upgrade === "websocket"` 426 check is unreachable once the
  dedicated 'upgrade' listener is registered: Node routes upgrade requests
  to 'upgrade' and never to the request handler. Verified on both the plain
  path and the MITM-decrypted (bili codex) path. Replace with a pointer comment.
- Add socket.on("error") to the 'upgrade' handler so a client vanishing
  mid-426-write cannot surface as an uncaught ECONNRESET.

Full suite 456/456, typecheck + build clean.
@ranxianglei

Copy link
Copy Markdown
Owner Author

LGTM on the core fix — and this one fixes a real, subtle bug, not a style issue.

Why the old code was dead: the 426 rejection lived in the request/handle() path checking req.headers.upgrade === "websocket". But Node.js intercepts WebSocket upgrade requests and routes them to the upgrade event — they never reach the request handler (this is version-dependent, which is exactly why the symptom was flaky). So Codex with supports_websockets=true was hitting a path that sometimes destroyed the socket with no response, hanging 10–20s before falling back to HTTP. The fix — a dedicated server.on("upgrade", ...) that writes a clean 426 over the raw socket and closes — is the correct and only reliable place to answer an upgrade. Removing the dead check from handle() is right.

The raw-socket write is correct: valid HTTP/1.1 response, Connection: close, correct Content-Length, and the socket.on("error", () => {}) guard prevents an ECONNRESET crash if the client vanishes mid-write. Both the plain and MITM-TLS paths are covered by the new tests/ws-upgrade.test.ts (174 lines), which is a good, targeted regression test. 455/455 pass.

One scope note (non-blocking): this PR also bundles a 212-line tests/e2e-grow-compress.test.ts that exercises the grow-and-compress pipeline end-to-end. It's a legitimate, passing test, but it's unrelated to the WS-426 fix — ideally it'd be its own commit/PR so the WS fix stays a one-line-of-intent change and is easier to bisect later. I'm fine merging it as-is since it's green and adds coverage, but flag it for future PRs: keep the WS fix and the e2e growth test separate.

Merge.

@ranxianglei
ranxianglei merged commit a0b8211 into master Aug 21, 2026
5 checks passed
@ranxianglei ranxianglei mentioned this pull request Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant