headroomlabs-ai/headroom · error · Error

Service reachable at ${explicitUrl}, but it does not appear

Error message

Service reachable at ${explicitUrl}, but it does not appear to be a Headroom proxy (${explicitProbe.reason ?? "unknown service"}).

What it means

The OpenClaw proxy manager probes the explicitly configured proxyUrl, found the address reachable, but the service did not identify itself as a Headroom proxy (probe.isHeadroom false, with a reason string). This is a deliberate guard against silently routing LLM traffic through an unrelated HTTP service. The probe reason in the message tells you what kind of service actually answered.

Source

Thrown at plugins/openclaw/src/proxy-manager.ts:100

      : null;
    const defaultCandidates = this.getDefaultProxyCandidates(port);
    const candidateUrls = explicitUrl ? [explicitUrl] : [...defaultCandidates];
    const probeByUrl = new Map<string, ProxyProbeResult>();

    for (const url of candidateUrls) {
      const probe = await probeHeadroomProxy(url);
      probeByUrl.set(url, probe);
      if (probe.reachable && probe.isHeadroom) {
        this.proxyUrl = url;
        this.logger.info(`Headroom proxy already running at ${url}`);
        return url;
      }
    }

    if (explicitUrl) {
      const explicitProbe = probeByUrl.get(explicitUrl);
      if (explicitProbe?.reachable && !explicitProbe.isHeadroom) {
        throw new Error(
          `Service reachable at ${explicitUrl}, but it does not appear to be a Headroom proxy (${explicitProbe.reason ?? "unknown service"}).`,
        );
      }
    }

    // Remote URLs are connect-only — never auto-start a subprocess for them
    if (explicitUrl && !isLocalProxyUrl(explicitUrl)) {
      throw new Error(
        `Remote Headroom proxy not reachable at ${explicitUrl}. Ensure the proxy is running at that address.`,
      );
    }

    // Auto-start is only available for local proxies
    if (this.config.autoStart === true) {
      const startupUrl = explicitUrl ?? defaultCandidates[0];
      const startupProbe = probeByUrl.get(startupUrl);
      if (startupProbe?.reachable && !startupProbe.isHeadroom) {
        throw new Error(

View on GitHub (pinned to 322425c43b)

Solutions

  1. Check what is actually listening on that port (lsof -i :PORT / the probe's reason string) and either stop it or move it
  2. Point proxyUrl at the real Headroom proxy's actual address/port
  3. If Headroom is supposed to be there, verify it is fully started — a half-started proxy can respond but fail the Headroom probe; retry after startup completes

Example fix

# before: another service on 8787
proxyManager.configure({ proxyUrl: "http://127.0.0.1:8787" });

# after: point at the actual Headroom port
headroom proxy --port 8790 &
proxyManager.configure({ proxyUrl: "http://127.0.0.1:8790" });
Defensive patterns

Strategy: validation

Validate before calling

import { probeHeadroomProxy } from "./proxy-probe.js";

// Before wiring config, confirm the target really is Headroom
const probe = await probeHeadroomProxy("http://127.0.0.1:8787");
if (!probe.reachable || !probe.isHeadroom) {
  throw new Error(`proxyUrl is not a Headroom proxy: ${probe.reason}`);
}

Type guard

interface Probe { reachable: boolean; isHeadroom: boolean; reason?: string }

function isHeadroomEndpoint(probe: Probe | undefined | null): probe is Probe & { reachable: true; isHeadroom: true } {
  return Boolean(probe && probe.reachable && probe.isHeadroom);
}

Try / catch

try {
  await manager.resolveProxyUrl();
} catch (e) {
  if (e instanceof Error && e.message.includes("does not appear to be a Headroom proxy")) {
    // wrong service on the port — surface the probe reason to the operator
    throw new Error(`Wrong service on proxy port: ${e.message}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Configuring proxyUrl to a port occupied by another HTTP service — a dev server, a different proxy, a metrics endpoint — while autoStart is not enabled. The probe succeeded at the TCP/HTTP level but the Headroom identification check (probeHeadroomProxy) failed.

Common situations: Default port 8787 already taken by another tool; stale config pointing at an old port where a different service now runs; container port-mapping mismatch where the host port maps to the wrong container port.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/581c482cad661559. Report an issue: GitHub.