schollz/croc · error · Error

Relay returned an invalid port list: ${banner}

Error message

Relay returned an invalid port list: ${banner}

What it means

After authenticating to the relay, the client parses the banner (the part before '|||' in the relay's response) as a comma-separated list of data-port numbers. If not a single token matches /^\d{1,5}$/, the banner is considered invalid and the error includes the raw banner text for diagnosis. The listed ports are where the parallel data connections will be opened.

Source

Thrown at web/src/protocol/client.ts:85

}

function controlPort(relayAddress: string) {
  try {
    const parsed = new URL(
      relayAddress.includes("://") ? relayAddress : `tcp://${relayAddress}`,
    );
    return parsed.port || CONTROL_PORT;
  } catch {
    return CONTROL_PORT;
  }
}

function dataPorts(banner: string) {
  const ports = banner
    .split(",")
    .map((port) => port.trim())
    .filter((port) => /^\d{1,5}$/.test(port));
  if (ports.length === 0) throw new Error(`Relay returned an invalid port list: ${banner}`);
  return ports;
}

function machineID() {
  const key = "croc-web-machine-id";
  try {
    const existing = localStorage.getItem(key);
    if (existing) return existing;
    const created = `web-${crypto.randomUUID()}`;
    localStorage.setItem(key, created);
    return created;
  } catch {
    return `web-${crypto.randomUUID()}`;
  }
}

async function connectRelay(
  settings: TransferSettings,

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Read the banner text embedded in the message: HTML/JSON means you reached the wrong server
  2. Point settings.relayAddress at a genuine croc relay (default croc relay) and verify with the croc CLI first
  3. If running a custom relay, make it send 'port1,port2,...|||external-ip' after the password handshake

Example fix

// before
const settings = { relayAddress: "https://mywebsite.com", ... };
// banner = "<html>404</html>" -> invalid port list

// after
const settings = { relayAddress: "croc-relay.example.com:9009", ... };
// banner = "9009,9010,9011,9012" -> parsed ok
Defensive patterns

Strategy: validation

Validate before calling

function isValidBanner(banner) {
  return banner.split(",").some((p) => /^\d{1,5}$/.test(p.trim()));
}
// Probe the relay with the croc CLI or a test handshake before starting a transfer
if (!isValidBanner(lastBanner)) throw new Error("Relay did not advertise data ports");

Try / catch

try {
  await sendFiles(opts);
} catch (e) {
  if (/invalid port list/.test(e.message)) {
    showBanner("Relay address is not a croc relay. Check settings.relayAddress.");
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Connecting through settings.gatewayURL to a service that is not a croc relay (an HTTP proxy or arbitrary WebSocket server) so the 'banner|||ip' payload is HTML or JSON; a relay implementation that returns an empty or named-host banner; a MITM/gateway rewriting the plaintext after decryption.

Common situations: Typo in the relay address pointing at a web server; a custom relay fork that changed the banner format; an API gateway in front of the relay injecting its own response.

Related errors


AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15). Data as JSON: /api/errors/bcf5ff9508215490. Report an issue: GitHub.