schollz/croc · error · Error
Relay rejected the connection: ${response}
Error message
Relay rejected the connection: ${response} What it means
During connectRelay the client completes a weak-key PAKE with the relay, derives a session key, and sends the relay password. The relay's reply must contain the '|||' separator between banner and external IP; anything else means the relay refused the handshake, and the raw decrypted response is included in the message. In practice the embedded text is the relay's rejection reason (e.g. 'incorrect password').
Source
Thrown at web/src/protocol/client.ts:123
port: string,
signal?: AbortSignal,
) {
const engine = wasm();
const socket = await CrocSocket.connect(settings.gatewayURL, port, signal);
try {
const pake = await engine.pakeInit(WEAK_RELAY_KEY, 0, "siec");
await socket.send(pake.bytes);
const peer = await socket.receive();
const finished = await engine.pakeUpdate(pake.handle, peer);
const salt = randomBytes(8);
const key = await engine.deriveKey(finished.key, salt);
await socket.send(salt);
await socket.send(await engine.encrypt(textEncoder.encode(settings.relayPassword), key));
const response = textDecoder.decode(
await engine.decrypt(await socket.receive(), key),
);
const separator = response.indexOf("|||");
if (separator < 0) throw new Error(`Relay rejected the connection: ${response}`);
const banner = response.slice(0, separator);
const externalIP = response.slice(separator + 3);
await socket.send(await engine.encrypt(textEncoder.encode(room), key));
const confirmation = textDecoder.decode(
await engine.decrypt(await socket.receive(), key),
);
if (confirmation !== "ok") {
throw new Error(`Relay could not open the room: ${confirmation}`);
}
return { socket, banner, externalIP } satisfies RelayConnection;
} catch (error) {
socket.close();
throw error;
}
}
async function sendControl(
socket: CrocSocket,View on GitHub (pinned to e25f1bdc04)
Solutions
- Read the response text inside the message; 'incorrect password' means the relay password is wrong
- Set settings.relayPassword to the exact passphrase the relay was started with (empty for none)
- Confirm the relay is a croc relay, not an arbitrary WebSocket endpoint
Example fix
// before
const settings = { relayAddress: "relay.example.com:9009", relayPassword: "" };
// relay started with: croc relay --passphrase hunter2
// after
const settings = { relayAddress: "relay.example.com:9009", relayPassword: "hunter2" }; Defensive patterns
Strategy: try-catch
Try / catch
try {
relay = await connectRelay(settings, room, port, signal);
} catch (e) {
if (/Relay rejected/.test(e.message)) {
throw new Error("Relay password incorrect. Update the relay password in settings.", { cause: e });
}
throw e;
} Prevention
- Store the relay password alongside the relay address so they change together
- Show the relay's embedded response text to the user; it states the reason
- Smoke-test credentials with the croc CLI before wiring them into the web app
When it happens
Trigger: settings.relayPassword not matching the relay's --passphrase; connecting to a relay that requires a password while sending the default empty one (or vice versa); the peer endpoint not actually being a croc relay.
Common situations: Self-hosted relays started with croc relay --passphrase; users switching between public and private relays without updating the password field; typos in the stored password.
Related errors
- Relay returned an invalid port list: ${banner}
- Relay could not open the room: ${confirmation}
- Message is too large (${payload.byteLength} bytes)
- Relay stream did not start with croc framing
- Stored-transfer origin must contain only an HTTPS scheme and
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/c212895e181a563d.
Report an issue: GitHub.