From f12d032008e0ea40c83c15b163c3003133e3b63b Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Wed, 20 May 2026 11:59:50 +0200 Subject: [PATCH] fix: Inject `--no-sandbox` into Stagehand's Chromium launch when sandbox is disabled Stagehand's `BrowserLaunchOptions` accepts `chromium_sandbox` but does not propagate it to the underlying Chromium launch, so containers running as root (e.g. Apify actors with `disable_browser_sandbox=True`) hit `ECONNREFUSED` on the CDP port. Inject `--no-sandbox` explicitly in `StagehandBrowserPlugin` so the `StagehandCrawler` actually starts a session on the Apify platform. --- .../browsers/_stagehand_browser_plugin.py | 9 ++++++++ .../browsers/test_stagehand_browser_plugin.py | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/crawlee/browsers/_stagehand_browser_plugin.py b/src/crawlee/browsers/_stagehand_browser_plugin.py index dec84295f2..6ae1370ae0 100644 --- a/src/crawlee/browsers/_stagehand_browser_plugin.py +++ b/src/crawlee/browsers/_stagehand_browser_plugin.py @@ -88,6 +88,15 @@ def __init__( if user_data_dir is not None: self._browser_launch_options['user_data_dir'] = str(user_data_dir) + # Stagehand's BrowserLaunchOptions accept `chromium_sandbox` but the field is not propagated + # to the underlying Chromium launch, so containers running as root hit ECONNREFUSED on the CDP port. + # Inject `--no-sandbox` explicitly when the sandbox is disabled. + if not self._browser_launch_options['chromium_sandbox']: + args = list(self._browser_launch_options.get('args') or []) + if '--no-sandbox' not in args: + args.append('--no-sandbox') + self._browser_launch_options['args'] = args + # Parameters for AsyncStagehand. self._stagehand_init_params: dict[str, Any] = { 'server': 'local' if is_local else 'remote', diff --git a/tests/unit/browsers/test_stagehand_browser_plugin.py b/tests/unit/browsers/test_stagehand_browser_plugin.py index 40cec7b472..abfc1f3364 100644 --- a/tests/unit/browsers/test_stagehand_browser_plugin.py +++ b/tests/unit/browsers/test_stagehand_browser_plugin.py @@ -64,6 +64,29 @@ def test_order_priority_of_implicit_options() -> None: assert plugin.browser_launch_options['viewport'] == {'width': 1280, 'height': 720} +def test_no_sandbox_added_when_sandbox_disabled() -> None: + # The autouse fixture in conftest sets CRAWLEE_DISABLE_BROWSER_SANDBOX=true. + plugin = StagehandBrowserPlugin() + + assert plugin.browser_launch_options['chromium_sandbox'] is False + assert '--no-sandbox' in plugin.browser_launch_options['args'] + + +def test_no_sandbox_not_added_when_sandbox_enabled(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv('CRAWLEE_DISABLE_BROWSER_SANDBOX', 'false') + plugin = StagehandBrowserPlugin() + + assert plugin.browser_launch_options['chromium_sandbox'] is True + assert 'args' not in plugin.browser_launch_options + + +def test_no_sandbox_not_duplicated_when_already_in_args() -> None: + plugin = StagehandBrowserPlugin(browser_launch_options={'args': ['--no-sandbox', '--disable-gpu']}) + + assert plugin.browser_launch_options['args'].count('--no-sandbox') == 1 + assert '--disable-gpu' in plugin.browser_launch_options['args'] + + def test_stagehand_options_defaults_when_not_provided() -> None: plugin = StagehandBrowserPlugin()