Skip to content

Session routes throw raw ParseError when id carries opencode: deep-link scheme prefix (opencode%3Ases_...) #31145

Closed as not planned

Description

@RobinVivant

Description

Session routes throw an unguarded Effect Schema ParseError when a session id arrives carrying the opencode: deep-link scheme prefix (URL-encoded as opencode%3A). The server logs:

ERROR service=server ref=err_cee4144f error=Expected a string starting with "ses", got "opencode%3Ases_169659db9ffeX6V47C8c3Sclz7" cause=Error: Expected a string starting with "ses", got "opencode%3Ases_169659db9ffeX6V47C8c3Sclz7"

opencode%3Ases_... is URL-encoded opencode:ses_.... Note the value contains a valid ses_... id - it just has the opencode://session/<id> deep-link scheme prefix prepended (added in #6232) and is never stripped/decoded before reaching the SessionID validator.

This is distinct from the existing placeholder-id reports (#28486 / #29262 got "dummy", #29868 got "%7Bid%7D" = uninterpolated {id}): those are placeholder strings, whereas this is a real id wrapped in the deep-link scheme. They do share a common theme - several code paths feed un-normalized strings straight into SessionID.make, which throws a raw Effect ParseError instead of degrading gracefully.

Root cause

The opencode: scheme prefix is never stripped (nor is the path segment decodeURIComponent'd) before the id reaches Schema.isStartsWith("ses"). Two paths fail:

1. Validator - packages/core/src/session/schema.ts:

export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
  Schema.brand("SessionID"),
  ...
)

(re-exported as SessionV2.ID, aliased SessionID in packages/opencode/src/session/schema.ts). The error text itself is formatted by Effect's ParseResult.ts (getDefaultTypeMessage -> Expected ${description}, actual ${value}), with the filter description a string starting with "ses".

2. Workspace-routing middleware - packages/opencode/src/server/shared/workspace-routing.ts (getWorkspaceRouteSessionID):

const id =
  url.pathname.match(/^\/session\/([^/]+)(?:\/|$)/)?.[1] ??
  url.pathname.match(/^\/experimental\/session\/([^/]+)\/background$/)?.[1]
if (!id) return null
return SessionID.make(id)   // id still carries the opencode: prefix -> throws

Called from .../httpapi/middleware/workspace-routing.ts for every /session/* route, so the throw fires before the handler.

3. Effect HttpApi path param - .../httpapi/groups/session.ts, every endpoint with params: { sessionID: SessionID } validates the raw :sessionID path segment the same way.

The only existing decodeURIComponent in the server (.../middleware/instance-context.ts) decodes the directory param, not session ids. There is no code that strips the opencode: scheme from a session id.

(Contrast: the TUI route TuiHttpApi.selectSession guards with if (!sessionID.startsWith("ses")) return BadRequest{}, so it fails cleanly there - but the workspace-routing / HttpApi paths surface the raw ParseError.)

Steps to reproduce

  1. Run opencode serve.
  2. Issue a session request whose path segment carries the deep-link scheme, e.g. a request derived from an opencode://session/<id> deep link, so the path contains opencode%3Ases_<id> (URL-encoded opencode:ses_<id>).
  3. The server responds with an unexpected error; logs show Expected a string starting with "ses", got "opencode%3Ases_...".

Suggested fix

Normalize (decode + strip scheme) before validation. Primary site - getWorkspaceRouteSessionID:

if (!id) return null
const decoded = decodeURIComponent(id)
const bare = decoded.startsWith("opencode:") ? decoded.slice("opencode:".length) : decoded
return SessionID.make(bare)

And apply the equivalent for the HttpApi params: { sessionID } path (e.g. a SessionIDFromPath transform schema that decodes + strips the scheme), so both entry points normalize identically. More broadly, consider having session routes return 400 BadRequest instead of letting SessionID.make throw a raw ParseError to the server error handler (the same hardening would improve #28486 / #29262 / #29868).

Related

OpenCode version

Observed on a build from 2026-06-05 (analysis pinned to commit 4519a1da on main); CLI 1.16.2 installed locally.

Activity

  1. tiffanychum commented on Jun 7, 2026

    @tiffanychum

    I'd like to work on this. Root cause: getWorkspaceRouteSessionID in workspace-routing.ts passes the raw URL path segment directly to SessionID.make() without decoding or stripping the opencode: scheme prefix. Fix: add normalization in workspace-routing.ts and a SessionIDFromPath transform schema for HttpApi params. PR coming shortly.

  2. github-actions commented on Aug 7, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

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