aio-libs/aiohttp · error · RuntimeError

Connection closed.

Error message

Connection closed.

What it means

Raised by StreamReader._wait when self._protocol.connected is False — i.e. a read coroutine (read/readany/readline/readchunk) is invoked after the transport has already closed. aiohttp refuses to await a future that can never be resolved because there is no live connection to feed it. The proper signal for end-of-stream is EOF (at_eof()), not a RuntimeError, so this is a genuine misuse.

Source

Thrown at aiohttp/streams.py:351

            return

        self._http_chunk_splits.append(self.total_bytes)

        # If we get too many small chunks before self._high_water is reached, then any
        # .read() call becomes computationally expensive, and could block the event loop
        # for too long, hence an additional self._high_water_chunks here.
        if len(self._http_chunk_splits) > self._high_water_chunks:
            self._protocol.pause_reading()

        # wake up readchunk when end of http chunk received
        waiter = self._waiter
        if waiter is not None:
            self._waiter = None
            set_result(waiter, None)

    async def _wait(self, func_name: str) -> None:
        if not self._protocol.connected:
            raise RuntimeError("Connection closed.")

        # StreamReader uses a future to link the protocol feed_data() method
        # to a read coroutine. Running two read coroutines at the same time
        # would have an unexpected behaviour. It would not possible to know
        # which coroutine would get the next data.
        if self._waiter is not None:
            raise RuntimeError(
                "%s() called while another coroutine is "
                "already waiting for incoming data" % func_name
            )

        waiter = self._waiter = self._loop.create_future()
        try:
            with self._timer:
                await waiter
        finally:
            self._waiter = None

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Read the body inside the `async with session.get(...) as r:` block before it exits.
  2. Check `await request.can_read_body()` / `request.body_exists` before reading.
  3. Catch RuntimeError and treat as end-of-stream if your flow tolerates abrupt close.
  4. Ensure no background task outlives the response context manager.

Example fix

// before
async with session.get(url) as r:
    data = r  # captured
return await data.content.read()  # connection already closed
// after
async with session.get(url) as r:
    return await r.read()
Defensive patterns

Strategy: validation

Validate before calling

# check liveness before reading:
if not request.content.is_eof() and not request.content._protocol.connected:
    raise ClientConnectionError('connection already closed')
if request.content.at_eof():
    return b''
return await request.content.read()

Try / catch

try:
    chunk = await stream.read(n)
except RuntimeError as e:
    if 'Connection closed' in str(e):
        return b''  # treat as EOF
    raise

Prevention

When it happens

Trigger: Calling await request.content.read(...) after request.release()/response close; reading a client response body after the session or connection was closed; using the same StreamReader from two lifecycles; reading after the server dropped the connection without sending EOF.

Common situations: Forgetting to keep the ClientSession / response context manager open while background task reads; calling read() in a finally block after the connection broke; reusing a request object across requests; webhook handler reading body after returning a response that closed the connection.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/4787f5a133a8711f.json. Report an issue: GitHub.