Skip to content

[Bug][Codex App] Large canonical Responses requests fail after v2.31.0 enables upstream WebSocket #2426

Closed

Description

@simon1561

Client or integration

Codex App

Area

Streaming

Summary

After upgrading from OpenCodex v2.28.0 to v2.31.0, one existing long-running Codex App task consistently fails through the canonical ChatGPT Responses route.

The request is approximately 18.2 MB as JSON. On v2.28.0, the same task and essentially the same request size completed successfully over the HTTP/SSE upstream path. On v2.31.0, the request is routed through the upstream Codex WebSocket and fails before any terminal Responses event:

Upstream stream terminated unexpectedly: codex websocket closed before a Responses terminal event

OpenCodex records status 502. I expected either the WebSocket path to complete successfully or OpenCodex to choose HTTP/SSE before opening the WebSocket when the outbound request is too large for the WS path.

Reproduction

  1. Run OpenCodex v2.31.0 with its bundled Bun 1.4.0.
  2. Use Codex App with the main ChatGPT account and the canonical ChatGPT Responses endpoint.
  3. Resume an existing tool-heavy task whose streaming /responses request body is approximately 18.2 MB.
  4. Send a short message.
  5. Codex App retries 5 times. Each upstream attempt closes before a terminal Responses event and OpenCodex returns 502.

Control observation:

  • OpenCodex v2.28.0 bundled Bun 1.3.14.
  • The same task, model, account, and approximately 18.2 MB request completed with HTTP 200 over HTTP/SSE.
  • The successful request was approximately 18,200,828 bytes before compression and 11,513,677 bytes after zstd.
  • After v2.31.0 starts with Bun 1.4.0, requests of approximately 18,197,133 bytes consistently fail on the upstream WebSocket path.
  • Other shorter conversations using the same account and model still succeed on v2.31.0, so the failure appears request-size/task-state specific rather than a general account, model, or network outage.
  • Setting websockets: false only disables the inbound Desktop-to-OpenCodex WebSocket and does not disable this upstream OpenCodex-to-ChatGPT WebSocket path.

The runtime gate appears relevant:

  • v2.28.0 package dependency: Bun 1.3.14
  • v2.31.0 package dependency: Bun 1.4.0
  • MIN_BOUNDED_CODEX_WS_BUN_VERSION = "1.4.0"
  • shouldUseCodexWsUpstream(...) therefore becomes active after the upgrade.
  • The upstream implementation sends the complete JSON request as one response.create WebSocket message.
  • I did not find an outbound body-size gate before selecting WS.

A safe mitigation may be a pre-send size-based fallback to HTTP/SSE, or an explicit upstream transport setting. Retrying over HTTP only after an opened WS closes may risk duplicate generation, so a decision before opening WS seems safer.

Version

2.31.0

Operating system

macOS 26.5.2 (Build 25F84), Apple Silicon

Provider and model

OpenAI ChatGPT account / gpt-5.6-luna / Responses

Logs or error output

OpenCodex: 2.31.0
Bundled Bun: 1.4.0
Codex CLI embedded in App: 0.149.0-alpha.4.1

Codex App:
stream disconnected before completion:
Upstream stream terminated unexpectedly:
codex websocket closed before a Responses terminal event

OpenCodex:
status: 502
first output: none
close reason: upstream WebSocket closed before a Responses terminal event
repeated attempts: approximately 3.8-5.3 seconds each

Screenshots and supporting files

No payload or screenshots attached because the request contains private task history. The sizes, version comparison, and redacted error above are reproducible from local logs.

Relevant release/runtime evidence:

Redacted configuration

{
  "port": 10100,
  "defaultProvider": "openai",
  "websockets": false,
  "providers": {
    "openai": {
      "adapter": "openai-responses",
      "baseURL": "https://chatgpt.com/backend-api/codex"
    }
  }
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, conversation IDs, request IDs, and personal data.

Activity

  1. added
    bugSomething isn't working
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    streamingSSE, WebSocket, terminal stream frames
    on Aug 23, 2026
  2. lidge-jun commented on Aug 23, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 64 / 80

    설명: 이 이슈는 코덱스 앱의 긴 일을 2.31.0 으로 올린 뒤, 예전에는 되던 큰 요청이 502 로 죽는다고 한다. 지금 CURRENT dev HEAD 는 4f41a8e93 이다. 이번 시간에 origin/dev 는 그대로다. 새 머지는 없다. 착지는 여전히 2396 사용량 CLI 오늘 비용이다. package.json 은 2.27.0 이다. src/config.ts 는 3975줄이다. src/runtime 폴더는 지금 HEAD 에 없다. 작성자는 메인 2.31.0 에서 재현했다. 지금 origin/dev 에도 같은 위 웹소켓 길이 있다.

    작성자는 2.28.0 에서 같은 일, 같은 계정, 같은 약 18.2 메가 요청이 에이치티티피 에스에스로 200 이 났다고 했다. 압축 전 약 18200828 바이트, 압축 후 약 11513677 바이트다. 2.31.0 은 번들 번이 1.4.0 이다. 같은 크기 약 18197133 바이트가 위 웹소켓에서 반복해서 죽는다. 짧은 대화는 같은 계정과 모델로 산다. 설정 websockets 를 꺼도 안 고쳐진다. 그 칸은 데스크톱에서 오픈코덱스로 들어오는 웹소켓만 끈다.

    지금 HEAD 의 src/server/responses/ws-upstream.ts 30줄 MIN_BOUNDED_CODEX_WS_BUN_VERSION 은 1.4.0 이다. 72줄 bunSupportsBoundedCodexWsRelay 는 1.3.14 에서 거짓이고 1.4.0 에서 참이다. 82줄 shouldUseCodexWsUpstream 은 번이 되고, 주소가 코덱스 응답 주소이고, 포스트이고, 본문이 글자이고, 맨 위 stream 이 참이면 위 웹소켓을 고른다. 본문 크기를 보는 칸은 없다. src/server/responses/fetch-helpers.ts 166줄이 그 함수가 참이면 위 웹소켓 길이로 보낸다. 설정 칸은 src/config.ts 2971줄 websocketsEnabled 와 3408줄 기본값 거짓이다. src/server/index.ts 889줄은 들어오는 업그레이드만 426 으로 막는다. 위로 나가는 길은 이 칸을 안 본다.

    위 길은 요청 제이슨 전체를 한 장의 response.create 로 보낸다. 11줄 주석, 125줄 stringify, 204줄 send 가 그 일이다. 열리기 전에 거절되거나, 보내기가 예외로 죽거나, 10초 안에 101 이 안 오면 에스에스로 넘어간다. 그때는 위 턴이 아직 없다. 열린 뒤에 위가 끊기면 300줄이 작성자가 본 그 문장을 낸다. 그때는 에스에스로 다시 보내지 않는다. 이미 위 턴이 시작됐을 수 있어서다. 29줄 MAX_CODEX_WS_QUEUE_BYTES 는 받는 큐 8메가다. 보내는 쪽 한도는 없다. 18.2 메가는 그 받는 한도보다 크다. 고치는 PR 은 아직 없다. 작성자가 말한 대로, 열기 전에 크기를 보고 에스에스로 가는 편이 다시 보내서 두 번 만드는 것보다 안전하다. 사용자 길이로는 긴 일이 502 로 끝나는 구멍이라서 64. 카탈로그 팁은 Ox Alpha x-preview-f-free + deepseek-v4-flash-vision-exp. Cursor 정적 카탈로그는 opus-4-8-fast / opus-5-fast. 2334 CursorCredentialRouter 는 여전히 src/providers/cursor-pool.ts 모듈+테스트만 있고 어댑터에 연결되지 않았다. 2332 H2 는 discovery 전용. 2320 overflow + 2342 는 이미 dev. 2188 사이드카는 이미 dev. 2382 데스크톱 앱 재시작은 이미 dev. 2292 는 아직 연다.

    src/server/responses/ws-upstream.ts 라인 82 - shouldUseCodexWsUpstream 은 본문 크기를 안 본다. 조건이 맞으면 무조건 위 웹소켓이다
    src/server/responses/ws-upstream.ts 라인 30 - 번 1.4.0 부터 이 길이 켜진다. 2.28.0 의 1.3.14 는 꺼져 있었다
    src/server/responses/ws-upstream.ts 라인 204 - 요청 전체를 한 장으로 보낸다. 보내는 쪽 한도는 없다
    src/server/responses/ws-upstream.ts 라인 300 - 열린 뒤에 위가 끊기면 그 문장을 내고 에스에스로 안 돌아간다
    src/server/responses/fetch-helpers.ts 라인 166 - 조건이 참이면 위 웹소켓 길이로 보낸다
    src/server/index.ts 라인 889 - 설정 websockets 는 들어오는 업그레이드만 끈다. 위로 나가는 길은 안 본다
    src/config.ts 라인 3408 - 그 설정 기본값은 거짓이다. 작성자 재현도 거짓이다

    메인테이너의 판단이 필요한 지점

    • 열기 전에 크기 한도를 두고 에스에스로 내릴지. 한도는 몇 바이트로 할지
    • 위로 나가는 길을 끄는 설정 칸을 따로 둘지. 지금 websockets 칸은 들어오는 문만 본다
    • 메인 2.31.0 핫픽스인지, origin/dev 에 먼저 넣을지. 지금 HEAD 에도 같은 구멍이 있다
    • 2210 과 한 이슈로 묶지 말 것. 제공자와 증상이 다르다

    너의 추천
    이슈는 열어 둔다. 고치는 PR 이 오면 그 PR 로 닫는다. 열기 전에 크기를 보고 에스에스로 내리는 쪽이 맞다. 열린 뒤에 다시 보내서 두 번 만들지 말 것. 호출 길을 넓히지 말 것. 2210 과 2279 는 닫지 않는다. 라벨은 그대로 둔다. 프리뷰 배포가 아니다.

    이 댓글은 grok-bot이 작성했습니다

  3. FacuM commented on Aug 23, 2026

    @FacuM

    Independent reproduction on Linux with the same failure shape.

    Environment

    • OpenCodex 2.31.0
    • Bundled Bun 1.4.0
    • Codex CLI 0.149.0
    • CachyOS, x86_64
    • Codex App
    • OpenAI ChatGPT account / gpt-5.6-sol / Responses

    Observed

    A long, tool-heavy Codex App thread began failing consistently after its request reached roughly 18.2 MB:

    stream disconnected before completion:
    Upstream stream terminated unexpectedly:
    codex websocket closed before a Responses terminal event
    

    The final retries all used the same request size:

    pre_compression_bytes=18244976
    post_compression_bytes=11454184-11454185
    retries=5/5
    first output: none
    

    The local OpenCodex endpoint returned HTTP 200 with text/event-stream, but the upstream WebSocket then closed without response.completed, response.failed, or response.incomplete. Retrying the normal turn repeatedly reproduced it. Remote compaction for the same thread also failed through the same upstream WebSocket path, so the thread could not self-recover.

    As a control, a fresh short request through the same OpenCodex instance, ChatGPT account, and gpt-5.6-sol route succeeded immediately during the incident. This rules out a general account/model/OpenAI service outage and makes the failure request-size/thread-state specific.

    I also confirmed the installed 2.31.0 implementation selects the upstream Codex WebSocket for every canonical streaming request without checking outbound body size, sends the complete request as one response.create frame, and only falls back to HTTP/SSE before the WebSocket has opened. This is consistent with the analysis already recorded here.

    No secrets, account identifiers, thread IDs, request IDs, or prompt contents are included.

  4. olddonkey commented on Aug 24, 2026

    @olddonkey
    Contributor

    Independent reproduction on Codex CLI (this report was filed separately as #2471 before I found this issue; #2471 is closed as a duplicate). Same mechanism, same version boundary. Adding the measurements that are missing here, since they pin the constant a size gate would need.

    The threshold is exactly 16 MiB

    Padding a request to specific byte counts through the local proxy:

    16,777,000 B = 16.000 MiB -> OK   (6.6s, response.completed)
    16,777,300 B = 16.000 MiB -> FAIL (1.0s, socket closed before any event)
    

    The boundary is 16,777,216 B = 16 MiB, reproducible byte-for-byte. So the pre-send gate proposed above should trip below 16 MiB, with headroom for any proxy-side field rewriting that happens after the size is measured.

    Confirmed with a real thread as well: 15 pasted screenshots (~1.4 MiB base64 each) = 20.9 MiB of images / 22.6 MiB of serialized items → 100% failure, ~1–2 s per attempt, streamAborted: true. Replaying that same thread's earlier 11-image snapshot (15.93 MiB) succeeds, which is a useful control — the failure tracks size, not thread content.

    It is the receiving side, not a Bun client cap

    A local Bun WS server with maxPayloadLength: 512 MiB accepts a 20 MiB frame from the same Bun client that fails against the backend. So Bun's outbound path is fine and the cap is enforced by chatgpt.com — consistent with WS close code 1009 Message Too Big.

    Two things worth folding into the fix

    1. Do not rotate accounts on this failure (relevant to the account-pool label). An oversized frame is deterministic, so a pool retry replays the same 20 MiB against every account in turn. router-for-me/CLIProxyAPI#4409 hit exactly this and recommends both halves: size-estimate before opening the WS, and suppress credential rotation on a 1009 close.

    2. The close code is currently discarded. In src/server/responses/ws-upstream.ts the close handler ignores event.code / event.reason, so a 1009 is indistinguishable from an ordinary network drop, and nothing in usage.jsonl or /api/logs records the real cause — they only show 502 / upstream_server_error / streamAborted. The actual message survives only in the client's rollout:

    ~/.codex/sessions/<date>/rollout-*.jsonl → task_complete.error
    "stream disconnected before completion: Upstream stream terminated unexpectedly:
     codex websocket closed before a Responses terminal event"
    

    Capturing code+reason and mapping an oversized close to its own errorCode would make this diagnosable without reading client-side files.

    On the post-open retry concern

    Agreed that a blanket HTTP retry after an opened WS closes risks duplicate generation, and that the decision belongs before the upgrade. Worth noting the narrow case that stays safe: socket opened, zero response.* events received, nothing streamed downstream — no upstream turn output existed, so a resend cannot double-generate. That is exactly the shape an oversized frame produces, and it would also cover a 1009 arriving from a payload the local gate under-measured.

    For reference: how upstream Codex handles the same wall

    Official Codex hits it too (openai/codex#24550: hard fallback once replacement_history reached ~17.38 MB with 42 inline images). Its three mitigations, in case they inform the roadmap here:

    1. HTTP/SSE fallback — core/src/client.rs logs falling back to HTTP (codex.transport.fallback_to_http) and sets disable_websockets once stream retries are exhausted.
    2. Client-side image budget / resize — resize_all_images, unified_image_budget, compaction_image_budget, "default high resized behavior". This is what actually keeps the ceiling out of reach; under full replay every pasted screenshot is permanent rent and ~11–12 of them cross 16 MiB.
    3. Within-turn incremental append — previous_response_id + get_incremental_items() over a reused socket. Important caveat if this ever comes up as a fix: it is strictly within one turn (fresh ModelClientSession per turn, store: false), so a turn's first frame still carries the full history. It reduces upload amplification across tool round-trips; it does not raise the 16 MiB ceiling.

    Environment

    opencodex 2.31.0 (npm global, bundled Bun 1.4.0), codex-cli 0.149.1, macOS (darwin 25.5.0), gpt-5.6-sol, service tier priority, ChatGPT auth. Independent of gpt-5.6-luna in the original report, so the failure is not model-specific.

  5. lidge-jun commented on Aug 24, 2026

    @lidge-jun
    Owner

    Landed via #2473 at 84ade0f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't workinglanded-via-maintainerOriginal PR closed after landing via a maintainer merge trainstreamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions