Repository navigation
Description
Summary
opencode upgrade / /update are unusable on networks where the TLS handshake
from Bun's HTTP client to opencode.ai is blackholed, even though plain curl
to the same URL works fine and the installer itself succeeds. The updater's
version check has no fallback transport, and passing an explicit version does
not skip it.
Environment
- opencode version: 2.2.3
- OS: Linux 5.14.0-687.59.1.el9_8.x86_64 (x86_64)
- Terminal: xterm-256color
- Shell: /bin/bash
- Install/channel: latest, installed via
curl -fsSL https://opencode.ai/v2/install | bash(method detected ascurl) - Active plugins: none
Reproduction
- On a network where Bun's TLS ClientHello to
opencode.aiis intercepted/blackholed (ISP-level DPI, common in RU), runopencode upgrade <any-version>. - Observe the command abort before downloading anything.
- Compare with
curl -fsSL https://opencode.ai/update/api/latest/cli/npm?current=2.2.3, which returns the JSON immediately.
Expected Behavior
- The update check should fall back to another transport (the install path already shells out to
curl) or at least retry. opencode upgrade <version>should not need the update API at all, since the target version is known.- The error should name the request that failed, not just "The operation timed out".
- In the worst case, the failure should degrade (e.g. "could not check, install latest anyway") rather than abort the whole command.
Actual Behavior
$ opencode upgrade 2.2.2
┌ Upgrade
│
● Using method: curl
│
● From 2.2.1 → 2.2.2
■ Upgrade failed
│
■ Could not check for OpenCode updates
│
│ The operation timed out.
│ Retry Check your network, then run opencode upgrade again.
│
└ Failed
Same result with a version that does not exist (opencode upgrade 9.9.9), so
the failure happens before/independently of anything version-specific. The TUI
/update fails identically.
Meanwhile the same machine can update successfully with the official installer,
because it only uses curl:
$ curl -fsSL https://opencode.ai/v2/install | bash -s -- --version 2.2.3 --no-modify-path
Installed version: 2.2.2
Installing opencode version: 2.2.3
... 100%
$ opencode --version
opencode v2.2.3
Additional Context
Measurements from the affected machine (same host, same minute):
| Check | Result |
|---|---|
Bun TCP connect to opencode.ai:443 (Bun.connect, tls: false) |
open in 45 ms |
Bun TLS connect to opencode.ai:443 (Bun.connect, tls: true) |
handshake never completes, no open and no error within 8 s |
curl https://opencode.ai/update/api/latest/cli/npm |
200, 0.4 s |
openssl s_client -connect opencode.ai:443 -tls1_3 |
handshake OK |
node fetch("https://opencode.ai/") |
200 |
Hosts where Bun's fetch hangs (all Cloudflare-fronted): opencode.ai,
models.dev, www.cloudflare.com, and the same IPs addressed directly with
SNI set to the IP literal.
Hosts where Bun's fetch works normally on the same machine: github.com,
api.github.com, registry.npmjs.org, ollama.com, example.com.
So the path itself is fine and the host is reachable; only Bun's TLS handshake
to those endpoints gets dropped by the network, which is outside opencode's
control. What is in opencode's control is that the update flow has a single hard
dependency on that fetch, with a 10 s AbortSignal and no fallback, while an
already-working curl-based path exists for the actual install.
Observations on the current implementation:
opencode upgradealways calls the update API to resolve the package name,
even when an explicit target version is provided, soopencode upgrade 2.2.2
cannot work offline.- The check runs with a fixed 10 s timeout and no retry, so slow-but-working
networks can fail the whole upgrade too. - The error text does not include the URL or the host that timed out, which
makes this hard to diagnose from a bug report. - Because Bun cannot reach
models.devhere either, the model catalog refresh
is affected by the same network behaviour (separate symptom, same root).
Suggested improvements: use curl (or an npm registry request) as a fallback
for the version check, skip the API entirely when a target version is passed,
include the failing host in the error, and allow configuring the transport.
Workaround used here: a small wrapper that resolves the latest version with
curl and then runs the official installer with --version, which works
without any proxy.
Activity
opencodejr commentedon Oct 30, 2026
Thanks for the detailed report. I could reproduce this on 2.2.3, and it still happens on the latest
dev(cfdb9da).To simulate your network, I pointed
opencode.aiat a local server that accepts the TCP connection and never completes the TLS handshake. I then ranopencode upgrade 2.2.2 --method curl, with a stand-incurlonPATHthat records each call:Could not check for OpenCode updates/The operation timed out., the same as your output.curlwas never called, so the upgrade stops before it even tries to download the installer.In the code,
opencode upgrade <version>always asks the update API for the package name. With the curl method that name is never used, because the installer only receives--version. So the update check is an unnecessary hard dependency there. The timeout error also doesn't say which URL it was trying. The script below reproduces it.Your other suggestions, a curl fallback, retries and a configurable transport, would be new behaviour rather than a fix for this bug. The underlying handshake problem is the same one discussed in #60399.
Reproduction script (repro.sh)
Run it:
bash repro.shzugmaster commentedon Oct 30, 2026
Adding a vantage-point test to pin down where this actually happens.
Same script, same code path, two networks:
hung 8sbun.sh/install:handshake OK 214msThe script used on both:
On the affected machine plain TCP to the same host opens in ~45 ms, while
curl,openssl s_client -tls1_3, and Node'sfetchall complete the TLS handshake and get a response from the same IP. So the endpoint and the Cloudflare edge are fine; only Bun's ClientHello never gets a reply from this particular network.Conclusion for the report: this is not a Bun or Cloudflare misconfiguration, something on the path silently drops the handshake for a specific ClientHello/host combination. Which is exactly why the updater should not treat a failed
fetchto its own API as fatal when acurl-based path is already available and working.zugmaster commentedon Oct 30, 2026
One more data point that shows what actually trips the filter, and why retries alone would not fix it.
Using
curl_cffi(same code, only the emulated TLS fingerprint changes) on the affected machine:Stepwise scan on
opencode.ai: chrome99 … chrome123 all succeed, chrome124 and newer all time out, Safari succeeds, curl (517-byte ClientHello) and Node succeed everywhere.Chrome 124 is exactly the release that enabled the post-quantum key share by default, and Bun bundles BoringSSL with PQ enabled, so Bun behaves like chrome124+ here.
Two things follow:
fetchwill not reliably help, while a non-PQ transport does — which is exactly what the installer already uses. So falling back tocurlfor the update check is not just a nicety, it is the difference between a working and a non-working update path on such networks.