schollz/croc · error · Error
Expected sender to close the file, got ${close.t}
Error message
Expected sender to close the file, got ${close.t} What it means
Thrown by the recipient when the sender's reply to 'close-sender' is neither 'error' nor 'close-recipient'. The per-file closing choreography requires the sender to acknowledge with close-recipient; any other message type means the sender's protocol state machine diverged (e.g. it sent the next recipientready itself, a finished message, or stale/duplicated frames arrived), so the recipient aborts before verifying and committing the file.
Source
Thrown at web/src/protocol/client.ts:799
});
});
const request: RemoteFileRequestWire = {
CurrentFileChunkRanges: [],
FilesToTransferCurrentNum: fileIndex,
MachineID: machineID(),
ReconnectVersion: 0,
};
await sendControl(control, {
t: "recipientready",
b: textEncoder.encode(JSON.stringify(request)),
}, key);
await receivePromise;
await sink.finalize();
await sendControl(control, { t: "close-sender" }, key);
const close = await receiveControl(control, key);
if (close.t === "error") throw new Error(close.m || "Sender cancelled");
if (close.t !== "close-recipient") {
throw new Error(`Expected sender to close the file, got ${close.t}`);
}
callbacks.onStatus?.(`Verifying ${file.path}`);
await verifySink(sink, file.hash);
await sink.commit();
totalTransferred = beforeFile + file.size;
callbacks.onFileComplete?.(file.path);
} catch (error) {
await sink.abort();
throw error;
}
}
await sendControl(control, { t: "finished" }, key);
const finishedMessage = await receiveControl(control, key);
if (finishedMessage.t !== "finished") {
throw new Error(`Expected transfer completion, got ${finishedMessage.t}`);
}
callbacks.onStatus?.("Transfer complete");View on GitHub (pinned to e25f1bdc04)
Solutions
- Log the unexpected type (embedded in the message) and compare with the expected choreography: close-sender -> close-recipient
- Use unique code phrases per transfer to prevent room collisions
- Ensure sender implementations always reply close-recipient after each file's close-sender
- Upgrade both peers so the closing sequence matches
Defensive patterns
Strategy: try-catch
Type guard
function isUnexpectedSenderClose(e: unknown): boolean {
return e instanceof Error && e.message.startsWith("Expected sender to close the file");
} Try / catch
catch (e) {
if (e instanceof Error && e.message.startsWith("Expected sender to close the file")) {
// protocol divergence at file close: abort; the stray type in the message names the cause
}
throw e;
} Prevention
- Implement the full per-file close handshake when writing a sender: close-sender in -> close-recipient out
- Use unique code phrases per transfer to keep the control channel private
- Pin peer versions; the close choreography is version-sensitive
When it happens
Trigger: Sender skips the close-recipient acknowledgement; duplicated or reordered control frames from the relay deliver an old message here; a non-conforming sender implementation replies with a different message; sender crash followed by relay garbage on the control socket.
Common situations: Custom/mock senders that end the file loop without close-recipient; relay duplication bugs; protocol drift between client versions; concurrent transfers colliding on a reused code phrase/room.
Related errors
- Expected recipient to close the file, got ${closed.t}
- Unexpected peer message: ${message.t}
- Recipient requested an unknown file
- Received file data before it was requested
- Sender did not provide file metadata
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/bc07cea31af3dc33.
Report an issue: GitHub.