Skip to content

Unknown non-/api routes return 200 text/html instead of 404, so API clients can't detect an unsupported endpoint #55704

Open

Description

@BartoszOsiej

Description

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.

Steps to reproduce

$ curl -i http://127.0.0.1:4096/global/health | head -3
HTTP/1.1 200 OK
content-type: text/html

<!doctype html><html lang="en" ...

$ curl -s -o /dev/null -w '%{http_code} %{content_type}\n' http://127.0.0.1:4096/api/bogus
404 application/json

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.

OpenCode version

2.0.29

Operating System

Arch Linux (any OS — server-side behaviour)

Activity

  1. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    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.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.

    Related:

  2. neriousy commented on Oct 23, 2026

    @neriousy
    Member

    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.

  3. Dante-dan commented on Oct 23, 2026

    @Dante-dan
    Contributor

    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?

  4. Yahhi commented on Nov 12, 2026

    @Yahhi

    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])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions