schollz/croc · error · Error

Unexpected peer message: ${message.t}

Error message

Unexpected peer message: ${message.t}

What it means

The sender's control loop accepts only 'error', 'finished', and 'recipientready' (with data b) messages after the file info is sent. Any other message type (or a recipientready missing its payload) throws with the offending type interpolated. This is the sender's guard against protocol divergence or a desynchronized control channel mid-transfer.

Source

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

    if (peerIP.t !== "externalip") throw new Error("Recipient did not secure the channel");
    await sendControl(control, { t: "externalip", m: relay.externalIP }, key);
    await sendControl(control, {
      t: "fileinfo",
      b: textEncoder.encode(JSON.stringify(senderInfo(files))),
    }, key);

    let totalTransferred = 0;
    for (;;) {
      checkAbort(signal);
      const message = await receiveControl(control, key);
      if (message.t === "error") throw new Error(message.m || "Recipient refused transfer");
      if (message.t === "finished") {
        await sendControl(control, { t: "finished" }, key);
        callbacks.onStatus?.("Transfer complete");
        return;
      }
      if (message.t !== "recipientready" || !message.b) {
        throw new Error(`Unexpected peer message: ${message.t}`);
      }

      const request = JSON.parse(
        textDecoder.decode(message.b),
      ) as RemoteFileRequestWire;
      const fileIndex = request.FilesToTransferCurrentNum;
      const prepared = files[fileIndex];
      if (!prepared) throw new Error("Recipient requested an unknown file");
      callbacks.onStatus?.(`Sending ${prepared.name}`);
      const beforeFile = totalTransferred;
      await sendFileData(
        prepared,
        request.CurrentFileChunkRanges,
        data,
        key,
        (fileBytes) => {
          totalTransferred = beforeFile + fileBytes;
          callbacks.onProgress?.({

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Ensure both ends run the same protocol build so only known message types are exchanged
  2. On any failure, tear down and restart the transfer cleanly instead of reusing sockets
  3. Log message.t from the error to identify which extension frame is being sent, and add handling or remove it

Example fix

// before (forked recipient sends an extra frame)
await sendControl(control, { t: "progress", pct: 50 }, key); // sender: Unexpected peer message: progress

// after: only send types the protocol defines
await sendControl(control, { t: "recipientready", b: encodedRequest }, key);
Defensive patterns

Strategy: type-guard

Type guard

function isHandledControlMessage(msg) {
  return !!msg && (msg.t === "error" || msg.t === "finished" ||
    (msg.t === "recipientready" && msg.b instanceof Uint8Array && msg.b.length > 0));
}

Try / catch

try {
  await sendFiles(opts);
} catch (e) {
  if (/^Unexpected peer message:/.test(e.message)) {
    logProtocolDivergence(e.message);
    showBanner("Transfer aborted: peer sent an unrecognized message. Ensure matching versions.");
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: A recipient implementation sending progress/ack frames the sender does not know; a 'recipientready' message with b unset; frames from a previous handshake left in the buffer after a reconnect.

Common situations: Custom croc forks adding new control message types; partial reconnects leaving stale frames; version skew introduced mid-stream.

Related errors


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