Skip to content

Invalid JSON/C causes "Unexpected server error" on startup #29200

Closed as not planned

Description

@HaleTom

Description

Any syntax error in opencode.jsonc (invalid JSONC, schema validation error) produces an unhelpful error message on startup:

Error: 4 of 6 requests failed: Unexpected server error. Check server logs for details.
Affected startup requests: config.providers, provider.list, app.agents, config.get

The actual error details (line/column of JSONC syntax errors, schema validation issues) are lost because ConfigParse.jsonc() throws JsonError and ConfigParse.effectSchema() throws InvalidError as plain exceptions, which become Effect defects and bubble up as a generic HTTP 500.

Plugins

none

OpenCode version

1.15.10

Steps to reproduce

  1. Create opencode.jsonc with invalid JSONC syntax (e.g., missing quote, or brace)
  2. Run opencode
  3. Observe the generic error message instead of the actual parse/validation error location

Example bad config:

}}{
  "permission": {
    "webfetch": {
      "*": "ask"

Screenshot and/or share link

N/A

Operating System

Linux

Terminal

N/A

Activity

  1. github-actions commented on May 25, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If you believe these are distinct issues, feel free to keep this open and link them for cross-reference.

  2. HaleTom commented on May 26, 2026

    @HaleTom
    Author

    PR #29238 vs #29200: Disambiguation

    The Assertion

    The duplicate-bot and issue summary claim that PRs #29238 and #29208 are duplicates, both addressing issue #29200 ("Invalid JSON/C causes 'Unexpected server error' on startup").

    The Reality

    These are not duplicates. They address the same root issue at different architectural layers with different UX tradeoffs. Both are valid and complementary.


    Issue #29200 (User Bug Report)

    Any syntax error in opencode.jsonc produces an unhelpful error message:

    Error: 4 of 6 requests failed: Unexpected server error. Check server logs for details.
    

    The actual error details (line/column of JSONC syntax errors, schema validation issues) are lost.


    PR #29208: Defense in Depth at Source

    Approach: Catch config errors at the config loading layer using Effect.catchCause inside Effect.sync.

    Location: packages/opencode/src/config/config.ts:420-456

    What it does

    Behavior Description
    Silent degradation When config parse/schema validation fails, return empty {} config
    Server starts Server starts with defaults, allowing users to recover
    Logging Errors are logged to server logs for debugging
    Scope Main config loading path only

    How it works

    const data = yield* Effect.sync(() => {
      const parsed = ConfigParse.jsonc(expanded, source)
      const cleaned = stripUnknownKeys(normalizeLoadedConfig(parsed, source))
      const result = ConfigParse.schema(Info, cleaned, source)
      parsedOk = true
      return result
    }).pipe(
      Effect.catchCause((cause) =>
        Effect.sync(() => {
          const errors = cause.reasons
            .filter(Cause.isDieReason)
            .map((r) => (r.defect instanceof Error ? r.defect.name : "UnknownError"))
            .join(", ")
          log.error("invalid config: config file could not be parsed", { path: source, error: errors })
          return {} as Info
        }),
      ),
    )

    Value

    1. Graceful startup: Server starts even with bad config
    2. Zero blocking: Users can fix config later
    3. Minimal changes: Single error boundary in config loading
    4. Mirrors existing pattern: Same approach already used in tui.ts

    Limitations

    • No client feedback: Clients don't know config is broken
    • Silent failures: Users might not realize config is invalid
    • Debugging: Only server logs show the issue

    PR #29238: Client-Facing Error Exposure

    Approach: Map config errors to structured HTTP 400 responses at the HTTP middleware layer.

    Location: packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts:22-25

    What it does

    Behavior Description
    Exposes errors Returns structured ConfigJsonError/ConfigInvalidError responses
    HTTP 400 Not a 500 — tells the client there's a problem
    Client guidance Error includes path and validation issues
    Scope All HTTP routes that might surface config defects

    How it works

    if (ConfigError.JsonError.isInstance(error) || ConfigError.InvalidError.isInstance(error)) {
      return Effect.succeed(HttpServerResponse.jsonUnsafe(error.toObject(), { status: 400 }))
    }

    Value

    1. Immediate feedback: Clients know config is broken immediately
    2. Actionable errors: Error includes path and specific issues
    3. Better UX: Client can prompt user to fix config
    4. Defensive: Catches any config errors that surface as defects through HTTP

    Limitations

    • Server must start first: Requires server to be running
    • Client-dependent: Only helps clients that make HTTP requests
    • Overhead: Adds error boundary to all HTTP routes

    Potential Conflicts

    1. UX Philosophy Conflict

    PR #29208 PR #29238
    "Start with defaults, let users fix later" "Tell users immediately there's a problem"
    Graceful degradation Fail loudly
    Server-side recovery Client-side awareness

    Resolution: These are complementary, not conflicting. Server startup should be graceful (#29208), but clients should be informed (#29238).

    2. Test Expectation Conflict

    The old test in httpapi-error-middleware.test.ts asserted:

    expect(response.status).toBe(500)
    expectUnknownErrorBody(body)

    PR #29238 changed this to:

    expect(response.status).toBe(400)
    expect(body).toEqual({
      name: "ConfigInvalidError",
      data: { path, issues [...] }
    })

    Resolution: The old test verified security (don't expose sensitive config paths). The new test verifies UX (expose structured error). Both are valid concerns.

    3. Layer Priority

    If both PRs are applied:

    1. Config errors caught at source (fix(config): catch parse errors gracefully during startup #29208) → empty config, server starts
    2. No HTTP errors occur → middleware never sees them

    This is actually a problem: PR #29238's value is diminished if PR #29208 catches everything.

    Resolution needed: Either:


    CLI Users

    PR #29238's HTTP middleware fix provides zero value for CLI users. The CLI invokes config loading directly via Config.use.get() in the main entry point, which calls the same config.ts code path that #29208 hardens. Any config errors are caught at the source and logged — they never traverse the HTTP layer where #29238's middleware sits.

    CLI users benefit only from #29208:

    • Server starts with defaults on bad config
    • Error logged to server logs
    • No blocking, no "Unexpected server error"

    HTTP clients benefit from both:

    If the goal is CLI-only fixes, #29238 is unnecessary. If the goal is end-to-end UX, both are required.


    Recommendation : Keep Both PRs (Current State)

    Rationale:

    1. fix(config): catch parse errors gracefully during startup #29208 is the primary fix: Graceful startup is the core requirement
    2. fix(opencode): expose config parse errors over http #29238 is defensive: Handles edge cases where config errors still surface
    3. Different attack surfaces: CLI vs HTTP API have different needs

    When #29208 catches everything: #29238 becomes redundant.

    When #29208 doesn't catch something: #29238 provides client feedback.


    Verdict

    Not duplicates. Both PRs address the same user-facing issue (#29200) but at different architectural layers:

    Aspect PR #29208 PR #29238
    Layer Config loading HTTP middleware
    Goal Graceful startup Client feedback
    Error handling Silently catch Expose as 400
    Scope Source Boundary
    UX for CLI Best N/A
    UX for HTTP clients None Best
    Defense in depth Layer 1 Layer 2

    Both bring value. Keep both unless #29208 proves to catch all config errors (in which case #29238 becomes redundant).

  3. HaleTom commented on May 26, 2026

    @HaleTom
    Author

    @YOMXXX any ideas based on the above summary?

    Note that tui.json error handling currently follows the same path that I'm implementing.

  4. imsquner commented on May 28, 2026

    @imsquner

    OpenCode desktop 500 "Unexpected server error" with ConfigJsonError

    Summary

    • UI request fails with 500 and ref err_ad5ee91b.
    • Server logs show repeated ConfigJsonError during config load.
    • Background dependency install fails for @opencode-ai/plugin@local.

    Environment

    • App: OpenCode 1.15.10 (packaged)
    • OS: Windows (win32, x64)
    • Electron: 41.2.1
    • Node: 24.14.1
    • Time: 2026-05-28 around 14:00 local

    Steps to Reproduce

    1. Launch OpenCode desktop.
    2. Trigger a request that hits the backend (details unknown).

    Expected

    Requests succeed and the server starts without errors.

    Actual

    UI shows:

    Error: Unexpected server error. Check server logs for details.
        at wrapClientError (oc://renderer/assets/main-CqEM7zM6.js:63862:12)
        at request (oc://renderer/assets/main-CqEM7zM6.js:60102:28)
        at async retry (oc://renderer/assets/main-CqEM7zM6.js:64628:14)
        at async Promise.all (index 0)
        at async oc://renderer/assets/main-CqEM7zM6.js:66214:11
    
    Reason:
    {
      "body": {
        "name": "UnknownError",
        "data": {
          "message": "Unexpected server error. Check server logs for details.",
          "ref": "err_ad5ee91b"
        }
      },
      "status": 500
    }
    

    Server Logs (redacted paths)

    WARN  2026-05-28T14:00:13 service=config dir=C:\Users\<redacted>\.config\opencode error=Cause([Fail(NpmInstallFailedError (cause: @opencode-ai/plugin: No matching version found for @opencode-ai/plugin@local.))]) background dependency install failed
    ERROR 2026-05-28T14:00:14 service=server ref=err_ad5ee91b error=ConfigJsonError cause=ConfigJsonError: ConfigJsonError
    

    Notes

    • Multiple ConfigJsonError entries appear during startup.
    • If needed, full logs are available in server-1/2026-05-28T140012.log and server-1/2026-05-28T140005.log.

    opencode-debug-20260528T140555.zip

  5. github-actions commented on Jul 28, 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