headroomlabs-ai/headroom · error · Error

Service reachable at

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.

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.

Appendix: 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)