python/cpython · error · RuntimeError

Cannot call write() after write_eof()

Error message

Cannot call write() after write_eof()

What it means

Raised by _SelectorSocketTransport.write() as RuntimeError when write() is called after write_eof() has already been invoked on the same transport. write_eof() half-closes the socket (shutdown(SHUT_WR)) marking that no more data will be sent; any subsequent write attempt violates that contract and is rejected instead of silently dropped.

Source

Thrown at Lib/asyncio/selector_events.py:1066

        except BaseException as exc:
            self._fatal_error(
                exc, 'Fatal error: protocol.eof_received() call failed.')
            return

        if keep_open:
            # We're keeping the connection open so the
            # protocol can write more, but we still can't
            # receive more, so remove the reader callback.
            self._loop._remove_reader(self._sock_fd)
        else:
            self.close()

    def write(self, data):
        if not isinstance(data, (bytes, bytearray, memoryview)):
            raise TypeError(f'data argument must be a bytes, bytearray, or memoryview '
                            f'object, not {type(data).__name__!r}')
        if self._eof:
            raise RuntimeError('Cannot call write() after write_eof()')
        if self._empty_waiter is not None:
            raise RuntimeError('unable to write; sendfile is in progress')
        if not data:
            return

        if self._conn_lost:
            if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES:
                logger.warning('socket.send() raised exception.')
            self._conn_lost += 1
            return

        if not self._buffer:
            # Optimization: try to send now.
            try:
                n = self._sock.send(data)
            except (BlockingIOError, InterruptedError):
                pass
            except (SystemExit, KeyboardInterrupt):

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Reorder logic so write_eof() is the very last operation on the transport, after all writes and drain() calls complete.
  2. Track EOF state in your protocol/handler and skip or queue-then-drop writes once EOF was sent.
  3. Cancel producer tasks before calling write_eof() so no concurrent write can race the half-close.
  4. Check transport.is_closing() and your own eof flag before writing from callbacks.

Example fix

// before
writer.write_eof()
writer.write(b"late data\n")  # RuntimeError

// after
writer.write(b"early data\n")
await writer.drain()
writer.write_eof()  # always last
Defensive patterns

Strategy: validation

Validate before calling

eof_sent = False

def safe_write(transport, data):
    if eof_sent or transport.is_closing():
        return False  # drop or raise your own error
    transport.write(data)
    return True

def close_write_side(transport):
    global eof_sent
    eof_sent = True
    transport.write_eof()

Try / catch

try:
    transport.write(data)
except RuntimeError as e:
    if 'write_eof' in str(e):
        log.debug('dropping write after EOF: %r', data[:64])
    else:
        raise

Prevention

When it happens

Trigger: Calling transport.write_eof() (or StreamWriter.write_eof()) and then transport.write(data) on the same connection; common when EOF is sent inside a finally block that runs before queued writes, or when a handler signals end-of-request then appends trailer data.

Common situations: HTTP/line protocols where the handler writes EOF early to signal end of request then tries to write a response; cleanup code that calls write_eof() on error paths while a background writer task is still producing data; bidirectional protocols misordering shutdown.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/f2313c415d005250. Report an issue: GitHub.