Skip to content

ensureCursorProxyServer health check fetch hangs indefinitely when port 32124 is unused #72

@Jcfunk

Description

@Jcfunk

Problem
The ensureCursorProxyServer function in plugin.ts performs a fetch("http://127.0.0.1:32124/health") with no timeout to check if a cursor proxy is already running. On systems where TCP SYN packets to unused ports on 127.0.0.1 are silently dropped (rather than returning ECONNREFUSED), this fetch hangs until the OS-level TCP timeout fires (~120–189s depending on tcp_syn_retries).
This effectively blocks plugin initialization for ~2 minutes before falling through to server.listen(), which then works fine.

Steps to Reproduce

  1. Install @rama_nigg/open-cursor (tested with v2.4.4)
  2. Ensure no existing cursor-acp proxy is running on port 32124
  3. Launch opencode (or any host that loads the plugin)
  4. Observe ~2 minute delay during plugin initialization

Expected vs. Actual Behavior

  • Expected: Health check returns quickly with an error (ECONNREFUSED or fetch error), and the plugin falls through to starting its own proxy server.
  • Actual: fetch() hangs for ~120–189s with no response, delaying entire plugin startup. Eventually the host's default HTTP timeout or OS TCP timeout fires and the plugin continues.

Technical Details

  • The hanging call is at plugin.ts:1127 inside ensureCursorProxyServer (the REUSE_EXISTING_PROXY branch, default true)
  • The fetch() has no timeout option set — no AbortSignal, no signal parameter
  • The .catch(() => null) on the fetch only handles rejected promises, not hanging connections
  • Works Around: set env with CURSOR_ACP_REUSE_EXISTING_PROXY=false
    skips the health check
  • Root cause: bind() to 127.0.0.1:32124 works fine; the issue is specifically connect() to a non-listening port on 127.0.0.1, which on some system configurations silently drops the SYN instead of sending RST

Suggested Fix
Add a short timeout (e.g. 2–3 seconds) to the health check fetch to prevent hanging:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
  const res = await fetch(`http://${host}:${port}/health`, {
    signal: controller.signal,
  }).catch(() => null);
  // ...
} finally {
  clearTimeout(timeout);
}

This ensures the health check fails fast if no proxy is running, regardless of the system's TCP behavior on 127.0.0.1.

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