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
- Read the banner text embedded in the message: HTML/JSON means you reached the wrong server
- Point settings.relayAddress at a genuine croc relay (default croc relay) and verify with the croc CLI first
- 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
- Default users to the standard croc relay unless a custom one is verified
- Validate relay connectivity at settings-save time, not mid-transfer
- Keep the relay and web client versions from the same release
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
- Relay rejected the connection: ${response}
- Message is too large (${payload.byteLength} bytes)
- Relay stream did not start with croc framing
- Relay could not open the room: ${confirmation}
- Peer sent an unexpected control handshake
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/bcf5ff9508215490.
Report an issue: GitHub.