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

  1. Read the response text inside the message; 'incorrect password' means the relay password is wrong
  2. Set settings.relayPassword to the exact passphrase the relay was started with (empty for none)
  3. 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

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


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