diff --git a/pythonlib/camoufox/async_api.py b/pythonlib/camoufox/async_api.py index 5cec0f2..7d9b70d 100644 --- a/pythonlib/camoufox/async_api.py +++ b/pythonlib/camoufox/async_api.py @@ -32,13 +32,24 @@ class AsyncCamoufox(PlaywrightContextManager): async def __aenter__(self) -> Union[Browser, BrowserContext]: _playwright = await super().__aenter__() - self.browser = await AsyncNewBrowser(_playwright, **self.launch_options) + try: + self.browser = await AsyncNewBrowser(_playwright, **self.launch_options) + except BaseException as e: + # Any launch failure (InvalidProxy, missing browser, bad options, ...) + # must tear down the playwright session started above so the driver + # process/connection is not leaked. + await super().__aexit__(type(e), e, e.__traceback__) + raise return self.browser async def __aexit__(self, *args: Any): - if self.browser: - await self.browser.close() - await super().__aexit__(*args) + # Run the base teardown even if browser.close() raises (e.g. the browser + # process already crashed), so the playwright driver/connection is not leaked. + try: + if self.browser: + await self.browser.close() + finally: + await super().__aexit__(*args) @overload diff --git a/pythonlib/camoufox/sync_api.py b/pythonlib/camoufox/sync_api.py index bbeade7..5763876 100644 --- a/pythonlib/camoufox/sync_api.py +++ b/pythonlib/camoufox/sync_api.py @@ -13,7 +13,6 @@ from typing_extensions import Literal from camoufox.virtdisplay import VirtualDisplay -from .exceptions import InvalidProxy from .fingerprints import generate_context_fingerprint from .utils import launch_options, sync_attach_vd @@ -33,15 +32,26 @@ class Camoufox(PlaywrightContextManager): super().__enter__() try: self.browser = NewBrowser(self._playwright, **self.launch_options) - except InvalidProxy as e: - super().__exit__(InvalidProxy, e, None) + except BaseException as e: + # Any launch failure (InvalidProxy, missing browser, bad options, ...) + # must tear down the playwright session started above. Leaking it leaves + # the sync API's event loop in a "running" state, so every later sync + # Camoufox/Playwright start in this thread fails with "Sync API inside + # the asyncio loop" until the process restarts (#82). + super().__exit__(type(e), e, e.__traceback__) raise return self.browser def __exit__(self, *args: Any): - if self.browser: - self.browser.close() - super().__exit__(*args) + # Run the base teardown even if browser.close() raises (e.g. the browser + # process already crashed). Skipping it leaks the sync API's event loop in a + # "running" state, so every later sync Camoufox/Playwright start in the same + # thread fails with "Sync API inside the asyncio loop" until process restart. + try: + if self.browser: + self.browser.close() + finally: + super().__exit__(*args) @overload