Repository navigation
[Bug][Codex App] Large canonical Responses requests fail after v2.31.0 enables upstream WebSocket #2426
Description
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
- Run OpenCodex v2.31.0 with its bundled Bun 1.4.0.
- Use Codex App with the main ChatGPT account and the canonical ChatGPT Responses endpoint.
- Resume an existing tool-heavy task whose streaming
/responsesrequest body is approximately 18.2 MB. - Send a short message.
- 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: falseonly 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.createWebSocket 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 eachScreenshots 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:
- v2.31.0 release: https://github.com/lidge-jun/opencodex/releases/tag/v2.31.0
- Bun bump commit: 27764f3
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
lidge-jun commentedon Aug 23, 2026
리뷰 · 우선순위 64 / 80
설명: 이 이슈는 코덱스 앱의 긴 일을 2.31.0 으로 올린 뒤, 예전에는 되던 큰 요청이 502 로 죽는다고 한다. 지금 CURRENT
devHEAD 는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 - 그 설정 기본값은 거짓이다. 작성자 재현도 거짓이다
메인테이너의 판단이 필요한 지점
너의 추천
이슈는 열어 둔다. 고치는 PR 이 오면 그 PR 로 닫는다. 열기 전에 크기를 보고 에스에스로 내리는 쪽이 맞다. 열린 뒤에 다시 보내서 두 번 만들지 말 것. 호출 길을 넓히지 말 것. 2210 과 2279 는 닫지 않는다. 라벨은 그대로 둔다. 프리뷰 배포가 아니다.
이 댓글은 grok-bot이 작성했습니다
FacuM commentedon Aug 23, 2026
Independent reproduction on Linux with the same failure shape.
Environment
Observed
A long, tool-heavy Codex App thread began failing consistently after its request reached roughly 18.2 MB:
The final retries all used the same request size:
The local OpenCodex endpoint returned HTTP 200 with
text/event-stream, but the upstream WebSocket then closed withoutresponse.completed,response.failed, orresponse.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-solroute 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.createframe, 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.
olddonkey commentedon Aug 24, 2026
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:
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 MiBaccepts 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-poollabel). 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.tstheclosehandler ignoresevent.code/event.reason, so a 1009 is indistinguishable from an ordinary network drop, and nothing inusage.jsonlor/api/logsrecords the real cause — they only show502 / upstream_server_error / streamAborted. The actual message survives only in the client's rollout:Capturing code+reason and mapping an oversized close to its own
errorCodewould 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_historyreached ~17.38 MB with 42 inline images). Its three mitigations, in case they inform the roadmap here:core/src/client.rslogsfalling back to HTTP(codex.transport.fallback_to_http) and setsdisable_websocketsonce stream retries are exhausted.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.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 (freshModelClientSessionper 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 tierpriority, ChatGPT auth. Independent ofgpt-5.6-lunain the original report, so the failure is not model-specific.lidge-jun commentedon Aug 24, 2026
Landed via #2473 at 84ade0f
feat(responses): opt-in ceiling for oversized outbound passthrough bo…
feat(responses): opt-in ceiling for oversized outbound passthrough bo…
feat(responses): opt-in ceiling for oversized outbound passthrough bo…
feat(responses): opt-in ceiling for oversized outbound passthrough bo…