kovidgoyal/kitty · error · TransmissionError

Cannot write to a closed file

Error message

Cannot write to a closed file

What it means

write_data raises this TransmissionError when data arrives for a DestFile that has already been closed (a prior end_data completed the transfer for that file_id). It guards against writing to a finalized entry.

Source

Thrown at kitty/file_transmission.py:517

        if self.mtime != FileTransmissionCommand.mtime:
            if is_symlink:
                with suppress(NotImplementedError):
                    os.utime(self.name, ns=(self.mtime, self.mtime), follow_symlinks=False)
            else:
                os.utime(self.name, ns=(self.mtime, self.mtime))

    def unlink_existing_if_needed(self, force: bool = False) -> None:
        if force or self.needs_unlink:
            with suppress(FileNotFoundError):
                os.unlink(self.name)
            self.existing_stat = None
            self.needs_unlink = False

    def write_data(self, all_files: dict[str, 'DestFile'], data: bytes | memoryview, is_last: bool) -> None:
        if self.ftype is FileType.directory:
            raise TransmissionError(code=ErrorCode.EISDIR, file_id=self.file_id, msg='Cannot write data to a directory entry')
        if self.closed:
            raise TransmissionError(file_id=self.file_id, msg='Cannot write to a closed file')
        if self.ftype in (FileType.symlink, FileType.link):
            self.link_target += data
            self.bytes_written += len(data)
            if is_last:
                lt = self.link_target.decode('utf-8', 'replace')
                base = self.make_parent_dirs()
                self.unlink_existing_if_needed(force=True)
                if lt.startswith('fid:'):
                    lt = all_files[lt[4:]].name
                    if self.ftype is FileType.symlink:
                        lt = os.path.relpath(lt, os.path.dirname(self.name))
                elif lt.startswith('fid_abs:'):
                    lt = all_files[lt[8:]].name
                elif lt.startswith('path:'):
                    lt = lt[5:]
                    if not os.path.isabs(lt) and self.ftype is FileType.link:
                        lt = os.path.join(base, lt)
                    lt = lt.replace('/', os.sep)

View on GitHub (pinned to 6d5d0c4406)

Solutions

  1. Make the sender idempotent: never emit data after end_data for a file_id.
  2. Deduplicate/resequence frames if going through a transport that can duplicate.
  3. Catch TransmissionError on the receiving side and drop the stale file_id gracefully.
Defensive patterns

Strategy: try-catch

Validate before calling

df = receiver.files.get(ftc.file_id)
if df is None or df.closed:
    return  # drop stale frame

Try / catch

try:
    receiver.add_data(ftc)
except TransmissionError as e:
    if 'closed' in (e.msg or ''): pass  # duplicate after end_data
    else: raise

Prevention

When it happens

Trigger: Sending action=data for a file_id after an action=end_data was already delivered for it — duplicate frames, retries, or out-of-order delivery in handle_receive_cmd -> add_data.

Common situations: Network/SSH retry logic resending the final chunk; a sender bug emitting end_data then more data; multiplexers duplicating escape sequences.

Related errors


AI-assisted analysis of kovidgoyal/kitty@6d5d0c4406 (2026-08-27). Data as JSON: /api/errors/1a3f54d9009845a2. Report an issue: GitHub.