{"record":{"id":"53842a3d44ab3565","repo":"D4Vinci/Scrapling","slug":"session-has-been-already-started","errorCode":null,"errorMessage":"Session has been already started","messagePattern":"Session has been already started","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"scrapling/engines/_browsers/_controllers.py","lineNumber":100,"sourceCode":"                elif self._config.proxy_rotator:\n                    self.browser = self.playwright.chromium.launch(**self._browser_options)\n                else:\n                    persistent_options = (\n                        self._browser_options | self._context_options | {\"user_data_dir\": self._user_data_dir}\n                    )\n                    self.context = self.playwright.chromium.launch_persistent_context(**persistent_options)\n\n                if self.context:\n                    self.context = self._initialize_context(self._config, self.context)\n\n                self._is_alive = True\n            except Exception:\n                # Clean up playwright if browser setup fails\n                self.playwright.stop()\n                self.playwright = None\n                raise\n        else:\n            raise RuntimeError(\"Session has been already started\")\n\n    def fetch(self, url: str, **kwargs: Unpack[PlaywrightFetchParams]) -> Response:\n        \"\"\"Opens up the browser and do your request based on your chosen options.\n\n        :param url: The Target url.\n        :param google_search: Enabled by default, Scrapling will set a Google referer header.\n        :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000\n        :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object.\n        :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need.\n        :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.\n        :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._\n        :param disable_resources: Drop requests for unnecessary resources for a speed boost.\n            Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`.\n        :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).\n        :param wait_selector: Wait for a specific CSS selector to be in a specific state.\n        :param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`.\n        :param network_idle: Wait for the page until there are no network connections for at least 500 ms.\n        :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute.","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/D4Vinci/Scrapling/blob/5d213a2d4764002bfc4fed33c32fe09fa8b0bf7f/scrapling/engines/_browsers/_controllers.py#L82-L118","documentation":"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.","triggerScenarios":"`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).","commonSituations":"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.","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."],"exampleFix":"# before\nsession = DynamicSession()\nwith session:\n    with session:  # RuntimeError: already started\n        session.fetch(url)\n\n# after\nwith DynamicSession() as session:\n    helper(session, url)  # helper uses session.fetch directly","handlingStrategy":"type-guard","validationCode":"def ensure_entered_once(session) -> bool:\n    return not bool(getattr(session, '_is_alive', False))","typeGuard":"def can_enter(session) -> bool:\n    \"\"\"True if the session is not already started (safe to __enter__).\"\"\"\n    return not bool(getattr(session, '_is_alive', False))","tryCatchPattern":"try:\n    with session:\n        session.fetch(url)\nexcept RuntimeError as e:\n    if 'already started' in str(e):\n        session.fetch(url)  # already live: just use it\n    else:\n        raise","preventionTips":["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."],"tags":["browser","lifecycle","context-manager","scrapling"],"backgroundTag":null,"analyzedSha":"5d213a2d4764002bfc4fed33c32fe09fa8b0bf7f","analyzedAt":"2026-08-14T22:23:09.440Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}