D4Vinci/Scrapling · error · RuntimeError

Session has been already started

Error message

Session has been already started

What it means

Raised by sync `DynamicSession.__enter__` (scrapling/engines/_browsers/_controllers.py:100) when the session is entered while already started. The `_is_alive` flag marks a live browser; entering twice would leak a second Playwright instance, so the second `__enter__` raises RuntimeError before launching.

Source

Thrown at scrapling/engines/_browsers/_controllers.py:100

                elif self._config.proxy_rotator:
                    self.browser = self.playwright.chromium.launch(**self._browser_options)
                else:
                    persistent_options = (
                        self._browser_options | self._context_options | {"user_data_dir": self._user_data_dir}
                    )
                    self.context = self.playwright.chromium.launch_persistent_context(**persistent_options)

                if self.context:
                    self.context = self._initialize_context(self._config, self.context)

                self._is_alive = True
            except Exception:
                # Clean up playwright if browser setup fails
                self.playwright.stop()
                self.playwright = None
                raise
        else:
            raise RuntimeError("Session has been already started")

    def fetch(self, url: str, **kwargs: Unpack[PlaywrightFetchParams]) -> Response:
        """Opens up the browser and do your request based on your chosen options.

        :param url: The Target url.
        :param google_search: Enabled by default, Scrapling will set a Google referer header.
        :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000
        :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object.
        :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need.
        :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads.
        :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` takes priority over the referer set here if used together._
        :param disable_resources: Drop requests for unnecessary resources for a speed boost.
            Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`.
        :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too).
        :param wait_selector: Wait for a specific CSS selector to be in a specific state.
        :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`.
        :param network_idle: Wait for the page until there are no network connections for at least 500 ms.
        :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute.

View on GitHub (pinned to 5d213a2d47)

Solutions

  1. Enter the session exactly once, at the outermost scope, and pass it down as a plain object.
  2. If helpers need lifecycle, give them the session without `with` — `do_fetch(session, url)` not `with session:`.
  3. Check `session._is_alive` (or track it yourself) before re-entering.
  4. Create a fresh session per nesting level instead of reusing one.

Example fix

# before
session = DynamicSession()
with session:
    with session:  # RuntimeError: already started
        session.fetch(url)

# after
with DynamicSession() as session:
    helper(session, url)  # helper uses session.fetch directly
Defensive patterns

Strategy: type-guard

Validate before calling

def ensure_entered_once(session) -> bool:
    return not bool(getattr(session, '_is_alive', False))

Type guard

def can_enter(session) -> bool:
    """True if the session is not already started (safe to __enter__)."""
    return not bool(getattr(session, '_is_alive', False))

Try / catch

try:
    with session:
        session.fetch(url)
except RuntimeError as e:
    if 'already started' in str(e):
        session.fetch(url)  # already live: just use it
    else:
        raise

Prevention

When it happens

Trigger: `with DynamicSession() as a: ...` nested inside another `with` on the same object; calling `__enter__` manually twice; re-entering a session stored on a class/module after the first `with` already set `_is_alive = True` (note: `__exit__` resets it, so the usual trigger is nested or manual double-entry while alive).

Common situations: Sharing one session object across helpers that each do `with session:`; wrapping the session in your own context manager that re-enters it; test fixtures that enter a module-level session multiple times.

Related errors


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