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
- Ensure both ends run the same protocol build so only known message types are exchanged
- On any failure, tear down and restart the transfer cleanly instead of reusing sockets
- 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
- Do not add control message types without updating the sender's loop on both ends
- Reuse sockets only within one clean handshake/transfer lifecycle
- Log the offending message type in telemetry to catch version skew in the field
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
- Recipient did not start a croc PAKE handshake
- Recipient did not secure the channel
- Expected recipient to close the file, got ${closed.t}
- Received file data before it was requested
- Expected sender to close the file, got ${close.t}
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/6c90bab4dc162665.
Report an issue: GitHub.