Skip to content

About

opencode v2 bug repro

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

OpenCode v2 drops the path prefix of a --server URL

Verified with the installed binaries:

  • opencode (v1): 1.18.25
  • opencode2 (v2): v0.0.0-beta-18743

Summary

A reverse proxy mounted under a path prefix works with the v1 client but fails with the v2 client. The v2 remote client resolves its root-relative /api/... paths with new URL(path, serverUrl), and a leading / replaces the whole path component of serverUrl — so the configured prefix is discarded.

new URL("/api/health", "http://127.0.0.1:3001/proxy").toString()
// => http://127.0.0.1:3001/api/health   (the /proxy prefix is gone)

Reproduction

repro.sh starts a real v1 server and a real v2 server, then path-proxy.ts (one bun process on :3000) mounts each under a path prefix, with a non-opencode root like Forge:

opencode  serve  :4096  (v1, unsecured)      proxy :3000/v1/* -> v1 server
opencode2 serve  :4097  (v2, password)       proxy :3000/v2/* -> v2 server
                                             proxy :3000/...  -> "running" (not opencode)

Each client is then pointed at its server directly (baseline) and through the path proxy. Run ./repro.sh.

Result

client (→ its server) direct through path proxy
opencode (v1) → v1 server ✅ ✅ …/v1/* (prefix kept)
opencode2 (v2) → v2 server ✅ ❌ fails (prefix dropped)

Both servers are reachable through the proxy (verified by curl: /v1/global/health and /v2/api/health both return healthy). Both clients work directly. Only the v2 client fails once its URL carries a path prefix.

v2 client — direct works, path fails

$ OPENCODE_SERVER_PASSWORD=… opencode2 api --server http://127.0.0.1:4097 GET /api/health
{"healthy":true,"version":"0.0.0-beta-18743","pid":38601}          # direct: OK

$ OPENCODE_SERVER_PASSWORD=… opencode2 api --server http://127.0.0.1:3000/v2 GET /api/health
ERROR: Server at http://127.0.0.1:3000/v2 did not provide a compatible V2 health response
  [cause]: ClientError: UnsupportedContentType                     # path: FAIL

The proxy access log for the failing run shows GET /api/health — the /v2 prefix was dropped, so it hit the non-opencode root, which returns running and is rejected. (The direct call proves the v2 server, client, and auth are all fine; only the path prefix differs.)

v1 client — path proxy keeps the prefix

opencode attach http://127.0.0.1:3000/v1 connects; every request stays under the prefix:

GET /v1/path
GET /v1/config/providers
GET /v1/global/event      <-- even the event stream
...

The v1 client joins base + path by concatenation (baseUrl + (path.startsWith("/") ? path : "/"+path)), preserving the prefix.

Source comparison

Repository: github.com/sst/opencode Branch: dev (the v2 line; the -v2 remote branches are feature branches) Commit: 04284921ac (latest origin/dev at time of writing)

V1 — string concatenation (preserves the prefix)

packages/sdk/js/src/gen/core/utils.gen.ts:96-97

const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
let url = (baseUrl ?? "") + pathUrl

baseUrl = "http://127.0.0.1:3001/proxy", _url = "/api/health" → http://127.0.0.1:3001/proxy/api/health. The prefix survives.

V2 — URL resolution (drops the prefix)

packages/app/src/utils/server-protocol.ts:14

const response = await fetch(new URL(path, server.url), {

called with root-relative paths at packages/app/src/utils/server-protocol.ts:28 and :31:

const legacy  = await probe(server, fetch, "/global/health")...   // line 28
const current = await probe(server, fetch, "/api/health")...      // line 31

new URL("/api/health", "http://127.0.0.1:3001/proxy") → http://127.0.0.1:3001/api/health. The leading / replaces the path component, so /proxy is discarded.

Introduced by commit d03e0c5e547f2bc7ae44e60eb21bfb24dad623fd ("feat(app): add dual-server compatibility", #38462).

It is broader than the probe: the main v2 client drops the prefix too

There are two v2 client generators, and the app uses both:

  • @opencode-ai/sdk/v2 — gen builder, concatenates → keeps the prefix (packages/sdk/js/src/v2/gen/core/utils.gen.ts:96-97). Used by createSdkForServer (packages/app/src/utils/server.ts:34).
  • @opencode-ai/client (OpenCode.make) — resolves with new URL → drops the prefix. This is the client used for health checks and the main API.

packages/client/src/generated/client.ts:

144:  const url = new URL(descriptor.path, options.baseUrl)   // origin-relative
253:  health: { … path: `/api/health` … }                     // all 61 endpoints are /api/...

new URL("/api/health", ".../proxy") → .../api/health, and the same for every endpoint (/api/session, /api/agent, …). When the origin returns non-JSON (Forge's running), this client raises UnsupportedContentType — the exact cause in the observed CLI error.

Prefix-losing call sites, all resolving /api/... against the origin:

  • packages/app/src/utils/server-protocol.ts:14 — protocol detection probe
  • packages/app/src/utils/server-health.ts — checkServerHealth, via OpenCode.make
  • packages/app/src/utils/server.ts:48 — createApiForServer, via OpenCode.make

The only prefix-preserving client (@opencode-ai/sdk/v2, string concat) is not the one doing health/detection/API. So the trailing path is dropped fundamentally, not just during health checks.

Where it runs (both cases reproduce)

  • Web UI — add/edit server: packages/app/src/components/dialog-select-server.tsx:262 (checkServerHealth) and :269 (detectServerProtocol); edit path at :313/:320.
  • Connection context (web + v2 CLI/TUI): packages/app/src/context/server-sdk.tsx:208 (detectServerProtocol). The app package is shared by the web front end and the v2 CLI.

Fix sites (exhaustive) and V1 status

An audit of every new URL(), fetch(), WebSocket, and EventSource request-builder in packages/client and packages/app finds exactly two that resolve endpoint paths against the origin (dropping the prefix). Both are new in V2; the V1 equivalents do not have the bug — which is why V1 never broke.

# V2 fix site Scope V1 status
1 packages/httpapi-codegen/src/index.ts:537 (codegen template) → generated packages/client/src/generated/client.ts:144 — new URL(descriptor.path, options.baseUrl) All 61 REST + SSE endpoints of @opencode-ai/client (health, sessions, agents, events) No bug. V1 uses a different, older client — @opencode-ai/sdk, packages/sdk/js/src/gen/core/utils.gen.ts:97 → (baseUrl ?? "") + pathUrl (concat, keeps prefix; created 2025-08-22). The new URL client is a V2 addition (2026-06-24, #33445).
2 packages/app/src/utils/server-protocol.ts:14 — fetch(new URL(path, server.url)) Protocol-detection probe Did not exist in V1 — added for V2 (2026-07-23, #38462).

Site 1 is a generated file, so fix the codegen template (then regenerate); that one change covers every endpoint of @opencode-ai/client, including the SSE event stream (which flows through the same prepare() → new URL).

The fix

Resolve endpoint paths relative to the configured base instead of against the origin — normalize the base to end in / and drop the path's leading slash:

function apiUrl(base: string | URL, path: string) {
  const b = new URL(base)
  if (!b.pathname.endsWith("/")) b.pathname += "/"
  return new URL(path.replace(/^\//, ""), b)   // relative join, prefix preserved
}
// apiUrl("http://127.0.0.1:3001/proxy", "/api/health") => .../proxy/api/health
// apiUrl("http://127.0.0.1:3001",       "/api/health") => .../api/health   (no regression)

Not fix sites (verified correct or unrelated)

  • packages/sdk/js/src/v2/gen/core/utils.gen.ts:96-97 — v2 gen SDK, concatenates (safe).
  • packages/app/src/utils/terminal-websocket-url.ts:16 — concatenates, already handles v1/v2 (safe).
  • server-sdk.tsx:194, entry.tsx:85, terminal.tsx:186/385/620, home-projects-view.tsx:253, deep-links.ts:7, draft-store.ts:157 — single-arg URL parsing, display, same-origin checks, or blob URLs; not request construction from server.url.

About

opencode v2 bug repro

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages