You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Unknown non-/api routes return 200 text/html instead of 404, so API clients can't detect an unsupported endpoint #55704
Any path that isn't a registered route is answered with the desktop SPA shell: 200 + text/html. API clients can't tell "this endpoint doesn't exist on this version" from "the server answered with a page", and the Accept header isn't checked on this path.
This matters for third-party clients that target the 1.x API (/session/..., /global/health). T3 Code's OpenCode driver, for example, ends up reporting "Request is not supported by this version of OpenCode Server (Server responded with text/html)" instead of a clean 404, and its own version gate passes 2.0.29 because it only compares semver >= 1.14.19.
So /api/* behaves correctly, but everything else falls through to the SPA handler.
Expected
Unknown paths under /api/ (and any non-browser request) return 404 with a JSON body, e.g. {"_tag":"NotFoundError","message":"..."}. Browsers navigating to those paths can still get the SPA.
This is how v2 is currently built. In packages/cli/src/services/web-ui.ts, only /api, /api/*, /auth/* and /openapi.json go to the API. Every other path gets the web app's index.html, so browser deep links and the signed-out sign-in screen work. v2 doesn't have the v1 routes (/global/health, /session/...), so those paths get the HTML page. One small detail: unknown /api/* routes currently return an empty 404, not a JSON body.
So I've filed this as a feature request: requests that clearly aren't from a browser (for example, an Accept header without text/html) would get a JSON 404 instead of the HTML page, and unknown /api/* routes would get a JSON error body. Browser navigation would still get the app. Maintainers will consider it.
For now, clients can detect a v2 server by calling GET /api/info. A 404 or HTML response from /api/info means the server isn't v2.
Additional diagnostic from #58145: a status-only health check is sufficient for clients to detect that /global/health returned SPA HTML rather than the expected API response. The third-party mobile pairing-flow changes remain with that app’s maintainers.
I’d like to take on the response-selection part of this feature. /api/info and the status-check workaround cover server detection; this proposal is narrower than fixing third-party pairing or restoring v1 endpoints.
In current web-ui.ts, non-API paths go straight to assets/the shell without consulting Accept; unknown API routes are converted to an empty 404. I propose:
Keep API/auth routing and authentication first for /api, /api/*, /auth/*, and /openapi.json. Only a route-not-found result becomes a JSON 404; valid responses, redirects, authentication errors and other failures keep their behavior. Serialize missing routes in the server’s existing tagged-error JSON style (_tag and message), using the existing RouteNotFound condition rather than a resource-specific error.
Keep exact embedded assets and existing missing _assets//icons/ handling before SPA negotiation. For an otherwise unmatched GET/HEAD, serve the shell only when an explicit text/html range has positive quality and its effective quality is at least that of application/json. Resolve specificity before quality, so text/html;q=0, */*;q=1 cannot enable HTML; compare JSON’s applicable exact/type-wildcard/global-wildcard range. Equal preference favors HTML. Invalid ranges do not enable the shell.
Missing Accept, wildcard-only ranges (including text/*), or an explicit preference for JSON get JSON 404. Sec-Fetch-* and user-agent strings would not override this rule: browser navigation works with its normal HTML Accept header, while a proxy stripping that header must preserve/set it to retain SPA fallback. Add Vary: Accept to negotiated responses without dropping existing Vary tokens. HEAD gets the corresponding status/headers and no body; preserve current non-GET/HEAD behavior.
For an unmatched /workspace/example:
Accept
Result
text/html or normal browser navigation Accept
SPA 200
application/json
JSON 404
text/html;q=0.5, application/json
JSON 404
text/html, application/json
SPA 200
text/html;q=0, */*
JSON 404
absent, */*, or text/* alone
JSON 404
The last row deliberately changes today’s fallback behavior, including shell fetches from tools or proxies that omit/purge Accept. It is a product compatibility choice, not something HTTP negotiation mandates. A more conservative alternative is to return JSON only for explicit JSON preference and retain today’s missing/wildcard fallback; that leaves headerless API clients receiving HTML. I recommend the explicit-HTML policy to cover the generic-client use case in this issue.
After design approval, I’d extend packages/cli/test/web-ui.test.ts with this matrix, weighted/specificity and malformed-range cases, authenticated unknown /api/* JSON bodies, GET/HEAD parity, and Vary behavior. Existing signed-out browser deep links, pairing redirects, API auth, assets/Brotli/cache/CSP and method tests remain regression boundaries. These are proposed checks; this is source analysis, not a tested patch.
Could a maintainer review the explicit-HTML default, especially missing/wildcard Accept compatibility?
Adding data from #63598 (closed as a duplicate of this one), as the bot suggested.
I measured how coding agents hit this. A real 2.2.7 server was running, and I asked Claude Code, Codex and Gemini (2 runs each) for a small script with "the official OpenCode JavaScript SDK or client from npm": create a session and list session titles.
3 of the 6 runs started with @opencode-ai/sdk 1.19.5. All three chose it from npm view and didn't open opencode.ai/docs.
Against the v2 server, client.session.list() returned the web UI's HTML as data, and client.session.create() returned {"error":{}} (405) without throwing.
Claude noticed only because a live server was there to test against. Without one, the v1 version would have shipped and returned [].
Codex searched opencode.ai first and went straight to @opencode/client.
So a JSON 404 for non-browser requests would turn this into a loud failure for agents too. One thing this issue doesn't cover yet: the agents picked the package on npm, so a pointer to @opencode/client on the @opencode-ai/sdk page (README, or an npm deprecate message if v1 servers no longer need it) would catch it before any code is written.
(I research how coding agents use dev tools: dev.to/yahhi · [email protected])
Activity
opencode-agent commentedon Oct 15, 2026
Thanks for the detailed report, @BartoszOsiej!
This is how v2 is currently built. In
packages/cli/src/services/web-ui.ts, only/api,/api/*,/auth/*and/openapi.jsongo to the API. Every other path gets the web app'sindex.html, so browser deep links and the signed-out sign-in screen work. v2 doesn't have the v1 routes (/global/health,/session/...), so those paths get the HTML page. One small detail: unknown/api/*routes currently return an empty 404, not a JSON body.So I've filed this as a feature request: requests that clearly aren't from a browser (for example, an
Acceptheader withouttext/html) would get a JSON404instead of the HTML page, and unknown/api/*routes would get a JSON error body. Browser navigation would still get the app. Maintainers will consider it.For now, clients can detect a v2 server by calling
GET /api/info. A 404 or HTML response from/api/infomeans the server isn't v2.Related:
/api/info, which aims at the same detection problem/api/*routes fell through to the HTML pageneriousy commentedon Oct 23, 2026
Additional diagnostic from #58145: a status-only health check is sufficient for clients to detect that
/global/healthreturned SPA HTML rather than the expected API response. The third-party mobile pairing-flow changes remain with that app’s maintainers.Dante-dan commentedon Oct 23, 2026
I’d like to take on the response-selection part of this feature.
/api/infoand the status-check workaround cover server detection; this proposal is narrower than fixing third-party pairing or restoring v1 endpoints.In current
web-ui.ts, non-API paths go straight to assets/the shell without consultingAccept; unknown API routes are converted to an empty 404. I propose:/api,/api/*,/auth/*, and/openapi.json. Only a route-not-found result becomes a JSON 404; valid responses, redirects, authentication errors and other failures keep their behavior. Serialize missing routes in the server’s existing tagged-error JSON style (_tagandmessage), using the existingRouteNotFoundcondition rather than a resource-specific error._assets//icons/handling before SPA negotiation. For an otherwise unmatched GET/HEAD, serve the shell only when an explicittext/htmlrange has positive quality and its effective quality is at least that ofapplication/json. Resolve specificity before quality, sotext/html;q=0, */*;q=1cannot enable HTML; compare JSON’s applicable exact/type-wildcard/global-wildcard range. Equal preference favors HTML. Invalid ranges do not enable the shell.Accept, wildcard-only ranges (includingtext/*), or an explicit preference for JSON get JSON 404.Sec-Fetch-*and user-agent strings would not override this rule: browser navigation works with its normal HTML Accept header, while a proxy stripping that header must preserve/set it to retain SPA fallback. AddVary: Acceptto negotiated responses without dropping existing Vary tokens. HEAD gets the corresponding status/headers and no body; preserve current non-GET/HEAD behavior.For an unmatched
/workspace/example:text/htmlor normal browser navigation Acceptapplication/jsontext/html;q=0.5, application/jsontext/html, application/jsontext/html;q=0, */**/*, ortext/*aloneThe last row deliberately changes today’s fallback behavior, including shell fetches from tools or proxies that omit/purge Accept. It is a product compatibility choice, not something HTTP negotiation mandates. A more conservative alternative is to return JSON only for explicit JSON preference and retain today’s missing/wildcard fallback; that leaves headerless API clients receiving HTML. I recommend the explicit-HTML policy to cover the generic-client use case in this issue.
After design approval, I’d extend
packages/cli/test/web-ui.test.tswith this matrix, weighted/specificity and malformed-range cases, authenticated unknown/api/*JSON bodies, GET/HEAD parity, and Vary behavior. Existing signed-out browser deep links, pairing redirects, API auth, assets/Brotli/cache/CSP and method tests remain regression boundaries. These are proposed checks; this is source analysis, not a tested patch.Could a maintainer review the explicit-HTML default, especially missing/wildcard Accept compatibility?
Yahhi commentedon Nov 12, 2026
Adding data from #63598 (closed as a duplicate of this one), as the bot suggested.
I measured how coding agents hit this. A real 2.2.7 server was running, and I asked Claude Code, Codex and Gemini (2 runs each) for a small script with "the official OpenCode JavaScript SDK or client from npm": create a session and list session titles.
@opencode-ai/sdk1.19.5. All three chose it fromnpm viewand didn't open opencode.ai/docs.client.session.list()returned the web UI's HTML asdata, andclient.session.create()returned{"error":{}}(405) without throwing.[].@opencode/client.So a JSON 404 for non-browser requests would turn this into a loud failure for agents too. One thing this issue doesn't cover yet: the agents picked the package on npm, so a pointer to
@opencode/clienton the@opencode-ai/sdkpage (README, or annpm deprecatemessage if v1 servers no longer need it) would catch it before any code is written.(I research how coding agents use dev tools: dev.to/yahhi · [email protected])