aio-libs/aiohttp · error · RuntimeError

Connection closed.

Error message

Connection closed.

What it means

Raised by StreamReader._wait when self._protocol.connected is False at the moment a read coroutine is about to await new data. It means the underlying transport/protocol has disconnected, so blocking for more data would hang forever; aiohttp raises instead. This typically surfaces wrapped in a ClientPayloadError or ServerDisconnectedError depending on context.

Solutions

  1. Treat a fully-read response as done: check response.content.at_eof() before reading more.
  2. Catch the error (often as aiohttp.ClientPayloadError or ServerDisconnectedError) and retry the request or close cleanly.
  3. Ensure you read the body exactly once and within the response context manager.

Example fix

# before
data = await response.content.read(1024)  # may raise after disconnect

# after
if not response.content.at_eof():
    data = await response.content.read(1024)
else:
    data = b''
Defensive patterns

Strategy: validation

Validate before calling

if response.content.at_eof() or not response.content._protocol.connected:
    data = b''
else:
    data = await response.content.read(n)

Type guard

def stream_readable(stream) -> bool:
    return getattr(stream, '_protocol', None) is not None and stream._protocol.connected and not stream.at_eof()

Try / catch

try:
    data = await response.content.read(n)
except (RuntimeError, aiohttp.ClientPayloadError, aiohttp.ServerDisconnectedError):
    data = b''  # connection gone; handle partial/no data

Prevention

When it happens

Trigger: Calling await response.content.read(n), readline(), or readuntil() after the server closed the connection (or after the response body was already fully consumed and the protocol detached); reading a request body on the server side after the client disconnected.

Common situations: Server closed keep-alive connection between reads; reading past Content-Length; proxy/load-balancer dropping an idle connection; client disconnect during server-side request body streaming.

Understand the failure class

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/4787f5a133a8711f. Report an issue: GitHub.

Appendix: 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 d041d4d0fd)