From 3a2ced8afcd61995b4f9c769a1d76bc2096769af Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Thu, 5 Nov 2026 00:55:28 +0200 Subject: [PATCH] feat: run without proxy locally when the proxy password is missing --- src/apify/_actor.py | 6 ++-- src/apify/_proxy_configuration.py | 32 +++++++++++++++---- .../test_actor_create_proxy_configuration.py | 14 ++++++++ tests/unit/test_proxy_configuration.py | 20 +++++++++++- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 526146cfd..bfead4415 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -1886,7 +1886,7 @@ async def create_proxy_configuration( Returns: ProxyConfiguration object with the passed configuration, or None, if no proxy should be used based - on the configuration. + on the configuration, or if Apify Proxy was requested in a local run without a password available. """ if actor_proxy_input is not None: if actor_proxy_input.get('useApifyProxy', False): @@ -1909,7 +1909,9 @@ async def create_proxy_configuration( _apify_client=self._apify_client, ) - await proxy_configuration.initialize() + is_proxy_usable = await proxy_configuration.initialize() + if not is_proxy_usable: + return None return proxy_configuration diff --git a/src/apify/_proxy_configuration.py b/src/apify/_proxy_configuration.py index e21b884f4..21b96a885 100644 --- a/src/apify/_proxy_configuration.py +++ b/src/apify/_proxy_configuration.py @@ -185,7 +185,7 @@ def __init__( self._country_code = country_code self._subdivision_code = subdivision_code - async def initialize(self) -> None: + async def initialize(self) -> bool: """Check if using proxy, if so, check the access. Load the Apify Proxy password from API (only if not passed to constructor or through env var). @@ -193,23 +193,41 @@ async def initialize(self) -> None: Only called if Apify Proxy configuration is used. Also checks if country has access to Apify Proxy groups if the country code is provided. + When Apify Proxy is used and no password is available, this raises on the Apify platform. In a local run + it logs a warning instead and the configuration falls back to running without a proxy. + You should use the Actor.create_proxy_configuration function to create a pre-initialized `ProxyConfiguration` instance instead of calling this manually. + + Returns: + `True` if the proxy configuration is usable, `False` if it fell back to running without a proxy. """ if self._uses_apify_proxy: if not self._password: await self._maybe_fetch_password() if not self._password: - raise ValueError( - 'Apify Proxy password must be provided using the "password" constructor argument ' - f'or the "{ApifyEnvVars.PROXY_PASSWORD}" environment variable. ' - f'You can also provide your Apify token via the "${ApifyEnvVars.TOKEN}" environment variable, ' - f'so that the SDK can fetch the proxy password from Apify API, ' - f'when not provided through constructor or ${ApifyEnvVars.PROXY_PASSWORD}.' + if self._configuration.is_at_home: + raise ValueError( + 'Apify Proxy password must be provided using the "password" constructor argument ' + f'or the "{ApifyEnvVars.PROXY_PASSWORD}" environment variable. ' + f'You can also provide your Apify token via the "${ApifyEnvVars.TOKEN}" environment ' + 'variable, so that the SDK can fetch the proxy password from Apify API, ' + f'when not provided through constructor or ${ApifyEnvVars.PROXY_PASSWORD}.' + ) + + logger.warning( + 'No proxy password or token detected, running without proxy. To use Apify Proxy locally, ' + f'provide the "password" constructor argument or the "{ApifyEnvVars.PROXY_PASSWORD}" ' + f'environment variable. You can also provide your Apify token via the "{ApifyEnvVars.TOKEN}" ' + 'environment variable, so that the SDK can fetch the proxy password from Apify API, ' + f'when {ApifyEnvVars.PROXY_PASSWORD} is not defined.' ) + return False await self._check_access() + return True + async def new_proxy_info( self, session_id: str | None = None, diff --git a/tests/unit/actor/test_actor_create_proxy_configuration.py b/tests/unit/actor/test_actor_create_proxy_configuration.py index 9d409ee20..dc6f13424 100644 --- a/tests/unit/actor/test_actor_create_proxy_configuration.py +++ b/tests/unit/actor/test_actor_create_proxy_configuration.py @@ -165,3 +165,17 @@ def request_handler(request: Request, response: Response) -> Response: assert call_mock.call_count == 3 await Actor.exit() + + +async def test_create_proxy_configuration_without_password_locally( + monkeypatch: pytest.MonkeyPatch, httpserver: HTTPServer +) -> None: + """Locally, requesting Apify Proxy without a password yields no proxy configuration.""" + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') + monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) + + async with Actor: + proxy_configuration = await Actor.create_proxy_configuration(groups=['GROUP1']) + + assert proxy_configuration is None + assert len(httpserver.log) == 0 diff --git a/tests/unit/test_proxy_configuration.py b/tests/unit/test_proxy_configuration.py index 2d6ed74ce..6b41cca77 100644 --- a/tests/unit/test_proxy_configuration.py +++ b/tests/unit/test_proxy_configuration.py @@ -438,7 +438,25 @@ def request_handler(request: Request, response: Response) -> Response: assert call_mock.call_count == 1 -async def test_initialize_without_password_or_token() -> None: +async def test_initialize_without_password_or_token( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, httpserver: HTTPServer +) -> None: + """Locally, a missing proxy password logs a warning, skips the access check and reports no usable proxy.""" + caplog.set_level('WARNING') + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') + monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) + + proxy_configuration = ProxyConfiguration() + + assert await proxy_configuration.initialize() is False + assert any('No proxy password or token detected, running without proxy.' in msg for msg in caplog.messages) + assert len(httpserver.log) == 0 + + +async def test_initialize_without_password_or_token_at_home(monkeypatch: pytest.MonkeyPatch) -> None: + """On the platform, a missing proxy password still raises.""" + monkeypatch.setenv(ApifyEnvVars.IS_AT_HOME.value, '1') + proxy_configuration = ProxyConfiguration() with pytest.raises(ValueError, match=r'Apify Proxy password must be provided'):