Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/apify/_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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

Expand Down
32 changes: 25 additions & 7 deletions src/apify/_proxy_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,31 +185,49 @@ 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).

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,
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/actor/test_actor_create_proxy_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 19 additions & 1 deletion tests/unit/test_proxy_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down
Loading