schollz/croc · error · Error

Expected recipient to close the file, got ${closed.t}

Error message

Expected recipient to close the file, got ${closed.t}

What it means

Thrown by the sender when, after sending all chunks of a file, the next control message is neither 'error' nor 'close-sender'. The recipient protocol requires it to acknowledge each completed file with close-sender; anything else (e.g. 'recipientready', 'finished', 'pake') means the recipient state machine is out of step with the sender's, so the sender aborts instead of desynchronizing further.

Source

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

        key,
        (fileBytes) => {
          totalTransferred = beforeFile + fileBytes;
          callbacks.onProgress?.({
            fileIndex,
            fileCount: files.length,
            fileName: prepared.name,
            fileBytes,
            fileSize: prepared.size,
            totalBytes: totalTransferred,
            totalSize,
          });
        },
        signal,
      );
      const closed = await receiveControl(control, key);
      if (closed.t === "error") throw new Error(closed.m || "Recipient cancelled");
      if (closed.t !== "close-sender") {
        throw new Error(`Expected recipient to close the file, got ${closed.t}`);
      }
      await sendControl(control, { t: "close-recipient" }, key);
      callbacks.onFileComplete?.(prepared.name);
    }
  } catch (error) {
    await reportPeerError(control, key, error);
    throw error;
  } finally {
    closeAll(control, data);
  }
}

type ActiveReceive = {
  file: OfferedFile;
  sink: ReceiveSink;
  received: Set<number>;
  bytes: number;
  queue: Promise<void>;

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Log the unexpected message type (already embedded in the message) and compare against the expected choreography: recipientready -> data -> close-sender -> close-recipient
  2. Ensure both peers run compatible versions of this web client / croc protocol; upgrade both sides
  3. Use a fresh, unique code phrase per transfer to avoid a third peer joining the room
  4. If writing a mock recipient, implement the full close-sender/close-recipient exchange per file
Defensive patterns

Strategy: try-catch

Type guard

function isUnexpectedCloseMessage(e: unknown): boolean {
  return e instanceof Error && e.message.startsWith("Expected recipient to close the file");
}

Try / catch

catch (e) {
  if (e instanceof Error && e.message.startsWith("Expected recipient to close the file")) {
    // log e.message — it names the stray message type; abort, never continue the loop
  }
  throw e;
}

Prevention

When it happens

Trigger: Recipient skips the close-sender handshake (buggy or older implementation); recipient sends the next recipientready before acknowledging the current file; a relay or proxy injects/reorders control frames; recipient crashes and another peer reuses the room, sending an unexpected control message.

Common situations: Interoperating with a modified croc CLI or non-standard client that omits the per-file close handshake; version drift where the close choreography changed; testing with a mock recipient that only implements the happy path; room collisions from reusing a short code phrase.

Related errors


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