Skip to content

Bug: plugin health check gets false positive from SPA HTML #64

@moyang-creator

Description

@moyang-creator

Summary

The getUrl() method in ServerManager.ts appends a base64-encoded project directory path to the OpenCode server URL. However, the OpenCode server does not recognize any path prefix — all requests with this prefix return the SPA HTML page instead of the expected JSON API response, causing the health check to always fail (false negative) and the iframe to load incorrectly.

Steps to Reproduce

  1. Install the plugin and configure it to start the OpenCode server
  2. Observe that the iframe spins indefinitely (does not load the OpenCode UI)
  3. Open Obsidian Developer Tools → Network tab
  4. Observe requests like:
    • GET http://127.0.0.1:4302/{base64-encoded-path}/global/health → returns HTTP 200 with Content-Type: text/html (SPA page)
    • POST http://127.0.0.1:4302/{base64-encoded-path}/session → also returns SPA HTML

Expected Behavior

  • GET http://127.0.0.1:4302/global/health should return JSON: {"healthy": true, "version": "1.15.4"} with Content-Type: application/json
  • The iframe should load the SPA at http://127.0.0.1:4302/ (without path prefix)
  • The health check should correctly validate server health

Root Cause

src/server/ServerManager.ts (L78-81):

getUrl(): string {
    const encodedPath = Buffer.from(this.projectDirectory).toString('base64');
    return `http://${this.settings.hostname}:${this.settings.port}/${encodedPath}`;
}

This method encodes the projectDirectory (the Obsidian vault root path) into the URL. But the OpenCode server (verified on v1.15.4) has no concept of a path-prefixed base URL. Running opencode serve --help shows only --port, --hostname, and --cors options — no base-path or project-directory URL prefix.

Health check also uses this URL:

checkServerHealth() {
    const url = `${this.getUrl()}/global/health`;
    return fetch(url, { method: "GET" })
        .then(r => 200 === r.status && "healthy" === r.statusText)
        .catch(() => false);
}

Since the SPA returns status 200 with statusText "OK" (not "healthy"), the check always fails.

Compounding issue: In src/main.ts, the getProjectDirectory() method uses:

const projectDirectory = this.settings.projectDirectory || this.app.vault.getRoot().path;

The || operator treats empty string as falsy, so even if the external launcher sets projectDirectory: "" in data.json, the plugin falls back to the vault path. This means the user cannot work around this bug from outside the plugin.

Impact

  • Plugin iframe never loads the OpenCode UI correctly
  • Health check never passes, causing the server to be repeatedly restarted in an infinite loop
  • The plugin is effectively non-functional when started via the plugin's launch mechanism

Proposed Fix

Modify getUrl() to return the raw server URL without encoding the project directory:

getUrl(): string {
    return `http://${this.settings.hostname}:${this.settings.port}`;
}

The projectDirectory is already correctly used as the server's cwd (working directory) when spawning the process — it does not need to be part of the URL. The health check would then correctly hit GET /global/health, and the iframe would load the SPA at the root URL.

Environment

  • Plugin version: latest (fetched from main branch)
  • OpenCode server: opencode-ai via npm
  • OS: Windows

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions