D4Vinci/Scrapling · error · RuntimeError

This FetcherSession instance already has an active…

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.

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.

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