aio-libs/aiohttp · error · ClientConnectionError

Connection closed

Error message

Connection closed

What it means

Raised as ClientConnectionError('Connection closed') from ClientResponse.read() when the response has already been explicitly released (_released=True) and you then call read(). After release(), the underlying connection is returned to the pool and the body is no longer accessible, so read() refuses to hand back stale/missing data. This is distinct from a network drop during the initial read.

Solutions

  1. Read the body before releasing: call await resp.read() (or .text()/.json()) while still inside the response context.
  2. Cache the bytes you need: body = await resp.read(); then use body later instead of resp.read() again.
  3. Avoid calling resp.release() manually if you intend to read later; let the context manager handle lifecycle.
  4. For streaming, consume resp.content fully before exiting the context.

Example fix

# before
async with session.get(url) as resp:
    pass
body = await resp.read()  # ClientConnectionError: Connection closed

# after
async with session.get(url) as resp:
    body = await resp.read()  # read while alive
# use `body` afterwards
Defensive patterns

Strategy: validation

Validate before calling

async def read_once(resp):
    if resp.closed or getattr(resp, '_released', False):
        raise RuntimeError('response already released; cannot read')
    return await resp.read()

# cache the body and reuse it instead of re-reading
async with session.get(url) as resp:
    body = await resp.read()
# use `body` later, not resp.read()

Type guard

def response_is_readable(resp) -> bool:
    return not getattr(resp, '_released', False) and not resp.closed

Try / catch

from aiohttp import ClientConnectionError

try:
    body = await resp.read()
except ClientConnectionError as e:
    if str(e) == 'Connection closed':
        # response was released; you must re-issue the request
        resp = await session.get(url)
        body = await resp.read()
    else:
        raise

Prevention

When it happens

Trigger: Calling resp.release() (or the context manager exiting and releasing), then calling resp.read() again on the same ClientResponse object. The _released flag is True and _body was never populated, so read() raises ClientConnectionError.

Common situations: Calling read() after the 'async with session.get() as resp' block has exited; calling release() manually then trying to read; storing the response object and using it after its lifecycle ended; mixing streaming (content.read()) with a later full read().

Understand the failure class

Related errors


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

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