schollz/croc · error · Error
Relay returned an invalid port list
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.
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
- Message is too large
- Relay rejected the connection
- Relay stream did not start with croc framing
- Expected recipient to close the file, got
- Expected sender to close the file, got
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/bcf5ff9508215490.
Report an issue: GitHub.
Appendix: 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)