D4Vinci/Scrapling · error · RuntimeError

This FetcherSession instance already has an active asynchron

Error message

This FetcherSession instance already has an active asynchronous session.

What it means

Raised by _ASyncSessionLogic.__aenter__ when the same asynchronous session instance is entered twice. The class supports exactly one live AsyncCurlSession per instance (tracked via _is_alive), so a second __aenter__ before __aexit__ is rejected. This mirrors the synchronous guard and protects the single curl handle from concurrent reuse.

Source

Thrown at scrapling/engines/static.py:419

        :return: A `Response` object.
        """
        # Careful of sending a body in a DELETE request, it might cause some websites to reject the request as per https://www.rfc-editor.org/rfc/rfc7231#section-4.3.5,
        # But some websites accept it, it depends on the implementation used.
        stealthy_headers = kwargs.pop("stealthy_headers", None)
        return self._make_request("DELETE", stealth=stealthy_headers, url=url, **kwargs)


class _ASyncSessionLogic(_ConfigurationLogic):
    __slots__ = ("_async_curl_session",)

    def __init__(self, **kwargs: Unpack[RequestsSession]):
        super().__init__(**kwargs)
        self._async_curl_session: Optional[AsyncCurlSession] = None

    async def __aenter__(self):  # pragma: no cover
        """Creates and returns a new asynchronous Session."""
        if self._is_alive:
            raise RuntimeError("This FetcherSession instance already has an active asynchronous session.")

        self._async_curl_session = AsyncCurlSession()
        self._is_alive = True
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Closes the active asynchronous session managed by this instance, if any."""
        # For type checking (not accessed error)
        _ = (
            exc_type,
            exc_val,
            exc_tb,
        )
        if self._async_curl_session:
            await self._async_curl_session.close()
            self._async_curl_session = None

        self._is_alive = False

View on GitHub (pinned to 5d213a2d47)

Solutions

  1. Enter the session once at the top level and pass it to coroutines that only call .get/.fetch, not __aenter__.
  2. Use a separate AsyncFetcherSession instance per concurrent task/context.
  3. For session-less usage, use AsyncFetcherClient, which builds one-off AsyncCurlSessions per request.

Example fix

# before
async def fetch_all(session, urls):
    async with session:  # second task hits RuntimeError
        ...

# after
async def fetch_all(session, urls):
    return await asyncio.gather(*(session.get(u) for u in urls))

async with AsyncFetcherSession() as session:
    await fetch_all(session, urls)
Defensive patterns

Strategy: validation

Validate before calling

async def open_once(session, coro_factory):
    if getattr(session, '_is_alive', False):
        return await coro_factory()  # already open: just use it
    async with session:
        return await coro_factory()

Type guard

def is_async_session_open(session) -> bool:
    return getattr(session, '_is_alive', False) is True

Try / catch

try:
    async with session:
        await session.get(url)
except RuntimeError as e:
    if 'already has an active' in str(e):
        await session.get(url)  # reuse the already-open session
    else:
        raise

Prevention

When it happens

Trigger: 'async with session:' nested inside another 'async with session:' on the same instance; awaiting session.__aenter__() twice manually; a shared AsyncFetcherSession entered concurrently by two tasks (e.g., asyncio.gather over a helper that opens the session).

Common situations: Refactoring a sync scraper that shared one session object across functions into async, where each coroutine opens the shared session; re-entering a session inside a retry wrapper that wraps the call in 'async with'.

Related errors


AI-assisted analysis of D4Vinci/Scrapling@5d213a2d47 (2026-08-14). Data as JSON: /api/errors/6af5b89058244017. Report an issue: GitHub.