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
- Enter the session exactly once, at the outermost scope, and pass it down as a plain object.
- If helpers need lifecycle, give them the session without `with` — `do_fetch(session, url)` not `with session:`.
- Check `session._is_alive` (or track it yourself) before re-entering.
- 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
- Enter each session exactly once, at the outermost scope.
- Don't wrap a shared session in `with` inside helpers.
- Track ownership explicitly in apps that share sessions.
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
- Context manager has been closed
- Browser not initialized for proxy rotation mode
- Session '{session_id}' is no longer alive. Open a new sessio
- No pages finished to clear place in the pool within the {sel
- Failed to get response for {url}
AI-assisted analysis of D4Vinci/Scrapling@5d213a2d47 (2026-08-14).
Data as JSON: /api/errors/53842a3d44ab3565.
Report an issue: GitHub.