aio-libs/aiohttp · error · RuntimeError

Session is closed

Error message

Session is closed

What it means

Raised at the top of ClientSession.request (and therefore every .get/.post/...) when self.closed is True. Once close() has run (or the async with block has exited) the session's connector is gone and no further requests can be made.

Solutions

  1. Create a new session for the new work: async with ClientSession() as s: ....
  2. Structure ownership so the session's lifetime encloses every task that uses it.
  3. Track closed state yourself and skip / reopen before calling request methods.

Example fix

// before
session = ClientSession()
await session.close()
await session.get('http://x')  # raises
// after
async with ClientSession() as session:
    await session.get('http://x')
# open a fresh session for further work
Defensive patterns

Strategy: try-catch

Validate before calling

async def safe_get(session, url):
    if session.closed:
        raise RuntimeError('session closed; open a new one')
    return await session.get(url)

Type guard

def is_open(session) -> bool:
    return not session.closed

Try / catch

try:
    resp = await session.get(url)
except RuntimeError as e:
    if 'Session is closed' in str(e):
        async with ClientSession() as new_session:
            resp = await new_session.get(url)
    else:
        raise

Prevention

When it happens

Trigger: Calling session.get(...) after await session.close(). Calling a request method on a session that was used as async with session: ... and the block has exited. Calling from a task that outlived the session's owner task.

Common situations: Reusing a module-level session after shutdown. Holding a session reference in a long-lived object whose cleanup ran. Test fixtures closing the session in teardown while an assertion still issues a request.

Related errors


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

Appendix: source

Thrown at aiohttp/client.py:503

        proxy: StrOrURL | None = None,
        timeout: ClientTimeout | _SENTINEL | None = sentinel,
        ssl: SSLContext | bool | Fingerprint | _SENTINEL = sentinel,
        server_hostname: str | None = None,
        proxy_headers: LooseHeaders | None = None,
        trace_request_ctx: object = None,
        read_bufsize: int | None = None,
        auto_decompress: bool | None = None,
        max_line_size: int | None = None,
        max_field_size: int | None = None,
        max_headers: int | None = None,
        middlewares: Sequence[ClientMiddlewareType] | None = None,
    ) -> ClientResponse:
        # NOTE: timeout clamps existing connect and read timeouts.  We cannot
        # set the default to None because we need to detect if the user wants
        # to use the existing timeouts by setting timeout to None.

        if self.closed:
            raise RuntimeError("Session is closed")

        method = method.upper()

        if ssl is sentinel:
            ssl = self._default_ssl
        if not isinstance(ssl, SSL_ALLOWED_TYPES):
            raise TypeError(
                "ssl should be SSLContext, Fingerprint, or bool, "
                f"got {ssl!r} instead."
            )

        if data is not None and json is not None:
            raise ValueError(
                "data and json parameters can not be used at the same time"
            )
        elif json is not None:
            if self._json_serialize_bytes is not None:
                data = payload.JsonBytesPayload(json, dumps=self._json_serialize_bytes)

View on GitHub (pinned to d041d4d0fd)