aio-libs/aiohttp · error · ClientConnectionError

Connection closed

Error message

Connection closed

What it means

Raised as ClientConnectionError in ClientResponse.read() when the response body has already been released (self._released is True) but read() is called again. After release(), the underlying connection is returned to the pool and the payload stream is gone, so there is nothing left to read.

Source

Thrown at aiohttp/client_reqrep.py:706

    async def _on_chunk_response_received(self, chunk: bytes) -> None:
        try:
            for trace in self._traces:
                await trace.send_response_chunk_received(self.method, self.url, chunk)
        except BaseException:
            self.close()
            raise

    async def read(self) -> bytes:
        """Read response payload."""
        if self._body is None:
            try:
                self._body = await self.content.read()
            except BaseException:
                self.close()
                raise
        elif self._released:  # Response explicitly released
            raise ClientConnectionError("Connection closed")

        protocol = self._connection and self._connection.protocol
        if protocol is None or not protocol.upgraded:
            await self._wait_released()  # Underlying connection released
        return self._body

    def get_encoding(self) -> str:
        ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower()
        mimetype = parse_mimetype(ctype)

        encoding = mimetype.parameters.get("charset")
        if encoding:
            with contextlib.suppress(LookupError, ValueError):
                return codecs.lookup(encoding).name

        if mimetype.type == "application" and (
            mimetype.subtype == "json" or mimetype.subtype == "rdap"
        ):

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Cache the result of the first read(): body = await resp.read() and reuse `body`.
  2. Avoid calling release() before you finish reading; let the context manager handle it.
  3. Keep all reads inside the `async with` block.
  4. If you need the body later, store it in a variable before exiting the context.

Example fix

# before
async with session.get(url) as resp:
    pass
data = await resp.read()  # response already released
# after
async with session.get(url) as resp:
    data = await resp.read()
# use `data` afterwards
Defensive patterns

Strategy: try-catch

Validate before calling

if resp._released:
    raise RuntimeError('response already released; use cached body')

Try / catch

from aiohttp import ClientConnectionError
try:
    body = await resp.read()
except ClientConnectionError as e:
    if 'Connection closed' in str(e):
        # response was released; re-fetch or use cached body

Prevention

When it happens

Trigger: Fires at line 705-706 when self._body is not None (so the first branch is skipped) but self._released flag is set. Happens when calling response.read()/text()/json() a second time after response.release() was invoked, or after the async context manager exited and released the response.

Common situations: Calling await response.read() after the `async with session.get(...)` block has already exited; mixing manual release() with later reads; retry logic that re-reads a released response.

Related errors


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