Repository navigation
[Bug] Streaming requests to opencode.ai hang with HTTP/2 SSE (200s timeout → 502/499, Codex stuck on 'thinking') #1668
Description
Client or integration
- Codex CLI
- Direct HTTP/API client (curl)
Area
- Proxy and routing
- Streaming
Summary
Requests proxied to opencode.ai (/zen/go/v1) hang forever in streaming mode (stream: true). The proxy waits the full 200s timeout, then reports connection reset / 502 upstream_server_error / 499 client_closed_request. On the client side (Codex CLI) the UI just shows "thinking" indefinitely.
This is not a proxy/DNS/network issue on the user's machine — all direct connections work fine. It is a protocol-level incompatibility between Bun's HTTP/2 fetch and the Cloudflare edge in front of opencode.ai for SSE streaming responses.
Reproduction
Same endpoint, same payload, only the HTTP version differs:
# HTTP/1.1 + stream=true → WORKS (SSE flows, ~3s)
curl --http1.1 -N https://opencode.ai/zen/go/v1/chat/completions \
-H "Authorization: Bearer <key>" -H "Content-Type: application/json" \
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}],"max_tokens":5,"stream":true}'
# → HTTP 200, SSE chunks received
# HTTP/2 + stream=true → HANGS (no bytes at all, times out)
curl --http2 -N https://opencode.ai/zen/go/v1/chat/completions \
-H "Authorization: Bearer <key>" -H "Content-Type: application/json" \
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}],"max_tokens":5,"stream":true}'
# → 0 bytes until client timeout (20s+)
# Bun fetch + stream=true (what opencodex actually does) → HANGS
# Bun's fetch negotiates HTTP/2 by default with this origin.Non-streaming (stream: false) works over both HTTP/1.1 and HTTP/2 (~2s).
opencodex forwards all chat completions with stream: true (confirmed via ocx debug provider logs):
[ocx:openai-chat:request] {"host":"opencode.ai","model":"deepseek-v4-flash","stream":true,...}
Version
2.15.0 (@bitkyc08/opencodex, bundled Bun 1.3.14, node_modules/bun/bin/bun.exe)
Operating system
macOS (darwin)
Provider and model
opencode.ai (opencode-go → https://opencode.ai/zen/go/v1, adapter openai-chat) / deepseek-v4-flash; Codex runtime: codex 0.147.0-alpha.6.5 (ChatGPT.app); outbound: direct connection (NO_PROXY set, no proxy vars).
Logs or error output
service.log: repeated[upstream-retry] connection reset (opencode.ai) — retrying (2/3)usage.jsonl(recent, ~100 requests):status=502 durationMs≈200000 errorCode=upstream_server_errorandstatus=499 closeReason=client_cancel(client abandons after ~90–160s)- Provider model discovery (
GET /models) succeeds; only the streaming POST hangs.
Redacted configuration
{
"provider": "opencode-go",
"baseURL": "https://opencode.ai/zen/go/v1",
"adapter": "openai-chat",
"model": "deepseek-v4-flash"
}Expected behavior
Streaming responses from opencode.ai should work over whatever HTTP version Bun negotiates. Either:
- opencodex should force HTTP/1.1 for SSE upstreams (Bun fetch supports
protocol: "http1.1"/"h1"), or - expose a config option to force HTTP/1.1 per provider, or
- investigate whether opencode.ai's Cloudflare edge is misbehaving with HTTP/2 SSE.
Notes
- Verified the opencodex process connects directly to
172.65.90.20:443(opencode.ai real IP, Cloudflare range) — no system/HTTP proxy involved. macOS system proxy (FlClash :7890) was ruled out via lsof and side-by-side tests. - Reproducible with both
opencode-zenandopencode-go(same originopencode.ai). - Worth checking if other Cloudflare-fronted SSE providers are affected by Bun's HTTP/2 fetch too.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
coderabbitai commentedon Aug 14, 2026
🔗 Related PRs
#749 - fix(responses): reject expired forward continuations [merged]
#966 - fix(codex): keep pre-connection DNS/network failures off account health (
#914) [open]#1006 - fix(deepseek): apply bounded JSON policy on HTTP SSE for Flash [open]
#1487 - perf: route ChatGPT Codex upstream turns over responses_websockets [open]
#1558 - perf: route ChatGPT Codex upstream turns over responses_websockets [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
github-actions commentedon Aug 14, 2026
Issue reopened
The report now contains the information required by the automated check. Thanks for updating it.
Wibias commentedon Aug 16, 2026
Triage relationship
Runtime roadmap parent: #1691.
This remains a separate HTTP/2/SSE interoperability symptom because it has its own reproduction and targeted mitigation path. Current implementation work is PR #1792.
fix(server): add per-provider upstreamHttpVersion to pin Bun fetch HT…
fix(config): canonicalize a null upstreamHttpVersion so the config st…
lidge-jun commentedon Aug 16, 2026
Mitigation shipped on
devby #1792.Each provider can now pin the HTTP version used for its upstream requests:
{ "providers": { "opencode": { "upstreamHttpVersion": "http1.1" } } }Values are
auto(default, Bun negotiates),http1.1/h1, orhttp2/h2, settable through the config file orPOST/PATCH /api/providers. Settinghttp1.1for the affected provider avoids the HTTP/2 SSE path where the stream stalls instead of delivering events. The option is documented in the provider configuration reference, since the whole point is that someone hitting this needs to know the knob exists.One contract defect was fixed before merging: the management API accepted
upstreamHttpVersion: nullas "clear this" and POST persisted it as submitted, while the loader schema rejected null — so a caller who took the API at its word got dropped into invalid-config recovery on the next start. POST now canonicalizes null to absent, and the loader accepts null and transforms it to undefined so any config the old path already wrote still loads. Regressions cover the POST → disk → reload round trip and a pre-existing null on disk.Scope, honestly stated: this is an operator-facing mitigation, not a root-cause fix for the stall itself. Whether the fault is Bun's HTTP/2 client, the provider's server, or an intermediary is still unresolved, and a long-running SSE smoke test against the affected provider on the pinned path is the remaining live confirmation. Closing the OpenCodex-side scope here — if the stall reproduces with
http1.1pinned, please reopen with that detail, since it would point somewhere other than HTTP/2 negotiation.Verified at the exact merge head: full Cross-platform CI green.
The change is on
devand ships with the next release; it is not in a published version yet.fix(server): add per-provider upstreamHttpVersion to pin Bun fetch HT…
fix(config): canonicalize a null upstreamHttpVersion so the config st…