different-ai/openwork · error · ApiError

opencode_unreachable

opencode_unreachable

Error message

OpenCode engine is unavailable

What it means

The OpenWork server proxies a request to the local OpenCode engine (opencode serve) and the underlying fetch failed with a connection-level failure. `isEngineConnectionFailure` classifies the transport error (ECONNREFUSED, socket close, timeout on connect) and `opencodeUnreachableError` converts it into ApiError code `opencode_unreachable`. This means the server could not reach the engine process backing the configured base URL, not that the route is wrong.

Solutions

  1. Check the engine process for the config is running (ps / process manager) and restart the server so the engine pool respawns it
  2. Verify the engine baseUrl/port in the config matches the actual opencode serve endpoint (curl the /health or root path)
  3. Inspect server logs for the underlying connection error recorded by reportRequestFailure to confirm ECONNREFUSED vs timeout
  4. If engine startup is slow, add readiness waiting/retry before the first proxy request

Example fix

// before
curl http://localhost:4096/session
// -> connect ECONNREFUSED
// after
opencode serve --port 4096 &
# then restart apps/server so enginePoolForConfig resolves the live baseUrl
curl http://localhost:4096/session
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(`${baseUrl}/`).catch(() => null);
if (!res) throw new Error("engine not reachable at " + baseUrl);

Type guard

function isOpencodeUnreachable(e: unknown): e is ApiError {
  return e instanceof ApiError && e.code === "opencode_unreachable";
}

Try / catch

try {
  await proxyRequest(req);
} catch (e) {
  if (isOpencodeUnreachable(e)) {
    showEngineDownBanner(); // surface restart guidance instead of raw failure
  } else throw e;
}

Prevention

When it happens

Trigger: A proxied request (method + targetUrl built from the engine baseUrl) throws from loopbackFetch with a connection failure; the pool's reportRequestFailure is also recorded for the workspace. Typical when the engine process crashed, was never started for the current config, or is still booting.

Common situations: OpenCode binary missing or crashed after startup; engine port changed or occupied; config points at a stale baseUrl; container/server started before the engine was ready; firewall or proxy blocking loopback.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/d865044b213f14d4. Report an issue: GitHub.

Appendix: source

Thrown at apps/server/src/server.ts:1490

      method,
      headers,
      body,
    }).then(() => {
      enginePoolForConfig(input.config)?.reportRequestSuccess(baseUrl);
    }).catch((error: unknown) => {
      if (workspace) enginePoolForConfig(input.config)?.reportRequestFailure(baseUrl, error, workspace);
      // Command failures are surfaced through the OpenCode event stream.
    });
    return jsonResponse({ ok: true, accepted: true });
  }
  const forward = async () => {
    let response: Response;
    try {
      response = await loopbackFetch(targetUrl, { method, headers, body });
      enginePoolForConfig(input.config)?.reportRequestSuccess(baseUrl);
    } catch (error) {
      if (workspace) enginePoolForConfig(input.config)?.reportRequestFailure(baseUrl, error, workspace);
      if (isEngineConnectionFailure(error)) throw opencodeUnreachableError(error, proxyPath);
      throw error;
    }

    if (response.status === 404 && route?.fallback) {
      const fallbackHeaders = headersForEngineConnection(headers, route.fallback);
      let fallbackResponse: Response;
      try {
        fallbackResponse = await loopbackFetch(
          buildOpencodeProxyUrl(route.fallback.baseUrl, proxyPath, search),
          { method, headers: fallbackHeaders, body },
        );
      } catch (error) {
        if (workspace) enginePoolForConfig(input.config)?.reportRequestFailure(route.fallback.baseUrl, error, workspace);
        if (isEngineConnectionFailure(error)) throw opencodeUnreachableError(error, proxyPath);
        throw error;
      }
      return sanitizeProxyResponse(fallbackResponse);
    }

View on GitHub (pinned to 2b7df46e8a)