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.

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.

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