From 3a2ced8afcd61995b4f9c769a1d76bc2096769af Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Thu, 5 Nov 2026 00:55:28 +0200 Subject: [PATCH 01/12] 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'): From 39928485f46bbd1099e31163f5b26e53d7780523 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 6 Nov 2026 12:23:57 +0100 Subject: [PATCH 02/12] test: isolate proxy password tests from the shell environment --- tests/unit/actor/test_actor_create_proxy_configuration.py | 2 ++ tests/unit/test_proxy_configuration.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/unit/actor/test_actor_create_proxy_configuration.py b/tests/unit/actor/test_actor_create_proxy_configuration.py index dc6f13424..e36ea5d97 100644 --- a/tests/unit/actor/test_actor_create_proxy_configuration.py +++ b/tests/unit/actor/test_actor_create_proxy_configuration.py @@ -171,6 +171,8 @@ 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.""" + monkeypatch.delenv(ApifyEnvVars.PROXY_PASSWORD.value, raising=False) + monkeypatch.delenv(ApifyEnvVars.TOKEN.value, raising=False) dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) diff --git a/tests/unit/test_proxy_configuration.py b/tests/unit/test_proxy_configuration.py index 6b41cca77..4489af142 100644 --- a/tests/unit/test_proxy_configuration.py +++ b/tests/unit/test_proxy_configuration.py @@ -443,6 +443,8 @@ async def test_initialize_without_password_or_token( ) -> None: """Locally, a missing proxy password logs a warning, skips the access check and reports no usable proxy.""" caplog.set_level('WARNING') + monkeypatch.delenv(ApifyEnvVars.PROXY_PASSWORD.value, raising=False) + monkeypatch.delenv(ApifyEnvVars.TOKEN.value, raising=False) dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) @@ -454,8 +456,10 @@ async def test_initialize_without_password_or_token( async def test_initialize_without_password_or_token_at_home(monkeypatch: pytest.MonkeyPatch) -> None: - """On the platform, a missing proxy password still raises.""" + """On the platform, a missing proxy password raises a `ValueError`.""" monkeypatch.setenv(ApifyEnvVars.IS_AT_HOME.value, '1') + monkeypatch.delenv(ApifyEnvVars.PROXY_PASSWORD.value, raising=False) + monkeypatch.delenv(ApifyEnvVars.TOKEN.value, raising=False) proxy_configuration = ProxyConfiguration() From 79fd75276e266377cf01e1bb12128c2f11c6df7d Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 6 Nov 2026 12:23:58 +0100 Subject: [PATCH 03/12] docs: describe what initialize() returns without a proxy password --- src/apify/_proxy_configuration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apify/_proxy_configuration.py b/src/apify/_proxy_configuration.py index 21b96a885..b970e96d7 100644 --- a/src/apify/_proxy_configuration.py +++ b/src/apify/_proxy_configuration.py @@ -194,13 +194,13 @@ async def initialize(self) -> bool: 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. + it logs a warning instead, skips the access check and returns `False`; the instance should then not be used. 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. + `True` if the proxy configuration is usable, `False` if the run should proceed without a proxy. """ if self._uses_apify_proxy: if not self._password: From 77f05890b528f5cdf068f52f1647c2aa07edf7c3 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 6 Nov 2026 12:23:59 +0100 Subject: [PATCH 04/12] fix: name only the missing password in the local proxy warning --- src/apify/_proxy_configuration.py | 2 +- tests/unit/test_proxy_configuration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apify/_proxy_configuration.py b/src/apify/_proxy_configuration.py index b970e96d7..d09d9766c 100644 --- a/src/apify/_proxy_configuration.py +++ b/src/apify/_proxy_configuration.py @@ -216,7 +216,7 @@ async def initialize(self) -> bool: ) logger.warning( - 'No proxy password or token detected, running without proxy. To use Apify Proxy locally, ' + 'No Apify Proxy password available, 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, ' diff --git a/tests/unit/test_proxy_configuration.py b/tests/unit/test_proxy_configuration.py index 4489af142..5093505fc 100644 --- a/tests/unit/test_proxy_configuration.py +++ b/tests/unit/test_proxy_configuration.py @@ -451,7 +451,7 @@ async def test_initialize_without_password_or_token( 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 any('No Apify Proxy password available, running without proxy.' in msg for msg in caplog.messages) assert len(httpserver.log) == 0 From 1c96ec446bc5c1a6720847479dff5e30213ff30d Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 6 Nov 2026 12:24:00 +0100 Subject: [PATCH 05/12] feat: run the Scrapy proxy middleware without a proxy locally --- src/apify/scrapy/middlewares/apify_proxy.py | 26 +++++++++++-------- .../scrapy/middlewares/test_apify_proxy.py | 20 +++++++++++++- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/apify/scrapy/middlewares/apify_proxy.py b/src/apify/scrapy/middlewares/apify_proxy.py index 7665bfcfa..b4f617b30 100644 --- a/src/apify/scrapy/middlewares/apify_proxy.py +++ b/src/apify/scrapy/middlewares/apify_proxy.py @@ -33,6 +33,7 @@ def __init__(self, proxy_settings: dict) -> None: """ self._proxy_settings = proxy_settings self._proxy_cfg_internal: ProxyConfiguration | None = None + self._runs_without_proxy = False @classmethod def from_crawler(cls: type[ApifyHttpProxyMiddleware], crawler: Crawler) -> ApifyHttpProxyMiddleware: @@ -66,6 +67,8 @@ def from_crawler(cls: type[ApifyHttpProxyMiddleware], crawler: Crawler) -> Apify async def process_request(self, request: Request) -> None: """Process a Scrapy request by assigning a new proxy. + In a local run without Apify Proxy credentials, the request is left without a proxy. + Args: request: Scrapy Request object. @@ -74,6 +77,9 @@ async def process_request(self, request: Request) -> None: """ url = await self._get_new_proxy_url() + if url is None: + return + if not (url.username and url.password): raise ValueError('Username and password must be provided in the proxy URL.') @@ -103,14 +109,14 @@ def process_exception( f'reason="{exception}", skipping...' ) - async def _get_new_proxy_url(self) -> ParseResult: + async def _get_new_proxy_url(self) -> ParseResult | None: """Get a new proxy URL. - Raises: - NotConfigured: If creation of the proxy configuration fails. - - Returns: New proxy URL. + Returns: New proxy URL, or None when the run goes without a proxy. """ + if self._runs_without_proxy: + return None + # Get proxy configuration, creating it if necessary proxy_cfg = ( self._proxy_cfg_internal @@ -118,13 +124,11 @@ async def _get_new_proxy_url(self) -> ParseResult: else await Actor.create_proxy_configuration(actor_proxy_input=self._proxy_settings) ) - # If the proxy configuration is still not available, raise an error. However, this should not happen due - # to the checks in the `from_crawler` method. + # `from_crawler` guarantees `useApifyProxy` is set, so None only comes from a local run without Apify Proxy + # credentials. `ProxyConfiguration.initialize` has already logged a warning about it. if proxy_cfg is None: - Actor.log.error( - 'Creation of proxy configuration failed. Check the field "proxyConfiguration" in the Actor input.' - ) - raise NotConfigured + self._runs_without_proxy = True + return None # Store the proxy configuration for future use self._proxy_cfg_internal = proxy_cfg diff --git a/tests/unit/scrapy/middlewares/test_apify_proxy.py b/tests/unit/scrapy/middlewares/test_apify_proxy.py index e6ad25b65..528b314e5 100644 --- a/tests/unit/scrapy/middlewares/test_apify_proxy.py +++ b/tests/unit/scrapy/middlewares/test_apify_proxy.py @@ -1,5 +1,6 @@ from __future__ import annotations +from unittest.mock import AsyncMock from urllib.parse import ParseResult, urlparse import pytest @@ -8,7 +9,7 @@ from scrapy.crawler import Crawler from scrapy.exceptions import NotConfigured -from apify import ProxyConfiguration +from apify import Actor, ProxyConfiguration from apify.scrapy.middlewares import ApifyHttpProxyMiddleware @@ -133,6 +134,23 @@ async def mock_get_new_proxy_url() -> ParseResult: await middleware.process_request(dummy_request) +async def test_process_request_without_proxy_configuration( + monkeypatch: pytest.MonkeyPatch, + middleware: ApifyHttpProxyMiddleware, + dummy_request: Request, +) -> None: + """Without a proxy configuration, requests go without a proxy and the creation is not retried.""" + create_proxy_configuration = AsyncMock(return_value=None) + monkeypatch.setattr(Actor, 'create_proxy_configuration', create_proxy_configuration) + + await middleware.process_request(dummy_request) + await middleware.process_request(Request('https://example.com/other')) + + assert 'proxy' not in dummy_request.meta + assert b'Proxy-Authorization' not in dummy_request.headers + create_proxy_configuration.assert_awaited_once() + + @pytest.mark.parametrize( 'exception', [TunnelError(), ValueError()], From cdcc934f42dca9a60a045302f77cdb9ae8487539 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 6 Nov 2026 12:24:01 +0100 Subject: [PATCH 06/12] docs: let the Crawlee guides run without a proxy configuration --- .../code/05_crawlee_beautifulsoup.py | 26 ++++++++++++------- docs/03_guides/code/05_crawlee_parsel.py | 22 +++++++++------- docs/03_guides/code/05_crawlee_playwright.py | 24 +++++++++++------ 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/docs/03_guides/code/05_crawlee_beautifulsoup.py b/docs/03_guides/code/05_crawlee_beautifulsoup.py index bdd1603b0..91ca173a1 100644 --- a/docs/03_guides/code/05_crawlee_beautifulsoup.py +++ b/docs/03_guides/code/05_crawlee_beautifulsoup.py @@ -1,6 +1,10 @@ import asyncio -from crawlee.crawlers import BeautifulSoupCrawler, BeautifulSoupCrawlingContext +from crawlee.crawlers import ( + BeautifulSoupCrawler, + BeautifulSoupCrawlingContext, + HttpCrawlerOptions, +) from crawlee.router import Router from apify import Actor @@ -42,17 +46,19 @@ async def main() -> None: Actor.log.info('No start URLs specified in Actor input, exiting...') await Actor.exit() - # Crawlee rotates the proxy URL per request on its own. + crawler_options: HttpCrawlerOptions[BeautifulSoupCrawlingContext] = { + 'request_handler': router, + # Cap the crawl. Remove or increase the limit to follow all links. + 'max_requests_per_crawl': 10, + } + + # Crawlee rotates the proxy URL per request on its own. A local run without + # Apify Proxy credentials gets no proxy configuration and crawls directly. proxy_configuration = await Actor.create_proxy_configuration() - if proxy_configuration is None: - raise RuntimeError('Failed to create the proxy configuration.') + if proxy_configuration is not None: + crawler_options['proxy_configuration'] = proxy_configuration - crawler = BeautifulSoupCrawler( - proxy_configuration=proxy_configuration, - request_handler=router, - # Cap the crawl. Remove or increase the limit to follow all links. - max_requests_per_crawl=10, - ) + crawler = BeautifulSoupCrawler(**crawler_options) await crawler.run(start_urls) diff --git a/docs/03_guides/code/05_crawlee_parsel.py b/docs/03_guides/code/05_crawlee_parsel.py index 8ccb24406..511a97958 100644 --- a/docs/03_guides/code/05_crawlee_parsel.py +++ b/docs/03_guides/code/05_crawlee_parsel.py @@ -1,6 +1,6 @@ import asyncio -from crawlee.crawlers import ParselCrawler, ParselCrawlingContext +from crawlee.crawlers import HttpCrawlerOptions, ParselCrawler, ParselCrawlingContext from crawlee.router import Router from apify import Actor @@ -42,17 +42,19 @@ async def main() -> None: Actor.log.info('No start URLs specified in Actor input, exiting...') await Actor.exit() - # Crawlee rotates the proxy URL per request on its own. + crawler_options: HttpCrawlerOptions[ParselCrawlingContext] = { + 'request_handler': router, + # Cap the crawl. Remove or increase the limit to follow all links. + 'max_requests_per_crawl': 10, + } + + # Crawlee rotates the proxy URL per request on its own. A local run without + # Apify Proxy credentials gets no proxy configuration and crawls directly. proxy_configuration = await Actor.create_proxy_configuration() - if proxy_configuration is None: - raise RuntimeError('Failed to create the proxy configuration.') + if proxy_configuration is not None: + crawler_options['proxy_configuration'] = proxy_configuration - crawler = ParselCrawler( - proxy_configuration=proxy_configuration, - request_handler=router, - # Cap the crawl. Remove or increase the limit to follow all links. - max_requests_per_crawl=10, - ) + crawler = ParselCrawler(**crawler_options) await crawler.run(start_urls) diff --git a/docs/03_guides/code/05_crawlee_playwright.py b/docs/03_guides/code/05_crawlee_playwright.py index df91b3f0a..eb714badd 100644 --- a/docs/03_guides/code/05_crawlee_playwright.py +++ b/docs/03_guides/code/05_crawlee_playwright.py @@ -1,6 +1,10 @@ import asyncio -from crawlee.crawlers import PlaywrightCrawler, PlaywrightCrawlingContext +from crawlee.crawlers import ( + BasicCrawlerOptions, + PlaywrightCrawler, + PlaywrightCrawlingContext, +) from crawlee.router import Router from apify import Actor @@ -42,21 +46,25 @@ async def main() -> None: Actor.log.info('No start URLs specified in Actor input, exiting...') await Actor.exit() - # Crawlee rotates the proxy URL per request on its own. + crawler_options: BasicCrawlerOptions[PlaywrightCrawlingContext] = { + 'request_handler': router, + # Cap the crawl. Remove or increase the limit to follow all links. + 'max_requests_per_crawl': 10, + } + + # Crawlee rotates the proxy URL per request on its own. A local run without + # Apify Proxy credentials gets no proxy configuration and crawls directly. proxy_configuration = await Actor.create_proxy_configuration() - if proxy_configuration is None: - raise RuntimeError('Failed to create the proxy configuration.') + if proxy_configuration is not None: + crawler_options['proxy_configuration'] = proxy_configuration # Common Chrome flags for running the browser in a container. browser_args = ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'] crawler = PlaywrightCrawler( - proxy_configuration=proxy_configuration, - request_handler=router, - # Cap the crawl. Remove or increase the limit to follow all links. - max_requests_per_crawl=10, headless=True, browser_launch_options={'args': browser_args}, + **crawler_options, ) await crawler.run(start_urls) From 35ac821493eac90c7a1a2b6fac039b9cb981f451 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 6 Nov 2026 12:24:01 +0100 Subject: [PATCH 07/12] docs: describe the local run without Apify Proxy credentials --- docs/02_concepts/05_proxy_management.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02_concepts/05_proxy_management.mdx b/docs/02_concepts/05_proxy_management.mdx index 5397714cd..8fb036ed0 100644 --- a/docs/02_concepts/05_proxy_management.mdx +++ b/docs/02_concepts/05_proxy_management.mdx @@ -20,7 +20,7 @@ The Apify SDK provides built-in proxy management through the `Actor.create_proxy_configuration()` logs a warning and returns `None`, and the Actor runs without a proxy. ### Using Apify Proxy From b7d7e8be396b4c623165038da760695028ab0575 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 10 Nov 2026 10:30:58 +0100 Subject: [PATCH 08/12] fix: run without proxy locally when proxy access is denied or the password fetch fails --- src/apify/_actor.py | 3 +- src/apify/_proxy_configuration.py | 58 ++++++++++----- .../test_actor_create_proxy_configuration.py | 18 +++++ tests/unit/test_proxy_configuration.py | 74 ++++++++++++++++++- 4 files changed, 131 insertions(+), 22 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index b8cc596b0..bb17a6e7c 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -1899,7 +1899,8 @@ async def create_proxy_configuration( Returns: ProxyConfiguration object with the passed configuration, or None, if no proxy should be used based - on the configuration, or if Apify Proxy was requested in a local run without a password available. + on the configuration, or if Apify Proxy was requested in a local run that cannot use it, for example + because no password is available or the proxy access is denied. """ if actor_proxy_input is not None: if actor_proxy_input.get('useApifyProxy', False): diff --git a/src/apify/_proxy_configuration.py b/src/apify/_proxy_configuration.py index d09d9766c..656ff8914 100644 --- a/src/apify/_proxy_configuration.py +++ b/src/apify/_proxy_configuration.py @@ -193,8 +193,9 @@ async def initialize(self) -> bool: 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, skips the access check and returns `False`; the instance should then not be used. + On the Apify platform, a missing password, a failed password fetch, or a denied proxy access raises. In a + local run, each of them logs a warning and returns `False` instead, and the run should proceed without a + proxy. Without a password, `new_proxy_info` and `new_url` return `None`. You should use the Actor.create_proxy_configuration function to create a pre-initialized `ProxyConfiguration` instance instead of calling this manually. @@ -210,21 +211,23 @@ async def initialize(self) -> bool: 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 ' + 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}.' + f'when not provided through constructor or {ApifyEnvVars.PROXY_PASSWORD}.' ) logger.warning( 'No Apify Proxy password available, 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.' + 'provide the "password" constructor argument or the "%s" environment variable. You can also ' + 'provide your Apify token via the "%s" environment variable, so that the SDK can fetch the ' + 'proxy password from Apify API, when %s is not defined.', + ApifyEnvVars.PROXY_PASSWORD, + ApifyEnvVars.TOKEN, + ApifyEnvVars.PROXY_PASSWORD, ) return False - await self._check_access() + return await self._check_access() return True @@ -260,16 +263,19 @@ async def new_proxy_info( return None if self._uses_apify_proxy: + if not self._password: + return None + parsed_url = URL(proxy_info.url) username = self._get_username(session_id) return ProxyInfo( - url=f'http://{username}:{self._password or ""}@{parsed_url.host}:{parsed_url.port}', + url=f'http://{username}:{self._password}@{parsed_url.host}:{parsed_url.port}', scheme='http', hostname=proxy_info.hostname, port=proxy_info.port, username=username, - password=self._password or '', + password=self._password, session_id=proxy_info.session_id, proxy_tier=proxy_info.proxy_tier, groups=self._groups, @@ -292,16 +298,22 @@ async def _maybe_fetch_password(self) -> None: token = self._configuration.token if token and self._apify_client: - user_info = await self._apify_client.user().get() + try: + user_info = await self._apify_client.user().get() + except Exception: + if self._configuration.is_at_home: + raise + logger.warning('Failed to fetch the Apify Proxy password using the Apify token.', exc_info=True) + return if user_info and (proxy := getattr(user_info, 'proxy', None)): self._password = proxy.password - async def _check_access(self) -> None: + async def _check_access(self) -> bool: proxy_status_url = f'{self._configuration.proxy_status_url}/?format=json' proxy_info = await self.new_proxy_info() if proxy_info is None: - return + return True status = None async with impit.AsyncClient(proxy=proxy_info.url, timeout=10) as client: @@ -314,16 +326,22 @@ async def _check_access(self) -> None: # retry on connection errors pass - if status: - if not status['connected']: - raise ConnectionError(status['connectionError']) - - self.is_man_in_the_middle = status['isManInTheMiddle'] - else: + if not status: logger.warning( 'Apify Proxy access check timed out. Watch out for errors with status code 407. If you see some, it ' 'most likely means you do not have access to either all or some of the proxies you are trying to use.' ) + return True + + if not status['connected']: + # A local setup may not be complete yet, so only the platform treats a denied access as fatal. + if self._configuration.is_at_home: + raise ConnectionError(status['connectionError']) + logger.warning('%s Running without proxy.', status['connectionError']) + return False + + self.is_man_in_the_middle = status['isManInTheMiddle'] + return True def _get_username(self, session_id: int | str | None = None) -> str: if session_id is not None: diff --git a/tests/unit/actor/test_actor_create_proxy_configuration.py b/tests/unit/actor/test_actor_create_proxy_configuration.py index e36ea5d97..17213374e 100644 --- a/tests/unit/actor/test_actor_create_proxy_configuration.py +++ b/tests/unit/actor/test_actor_create_proxy_configuration.py @@ -181,3 +181,21 @@ async def test_create_proxy_configuration_without_password_locally( assert proxy_configuration is None assert len(httpserver.log) == 0 + + +@pytest.mark.usefixtures('patched_impit_client') +async def test_create_proxy_configuration_when_not_connected_locally( + monkeypatch: pytest.MonkeyPatch, httpserver: HTTPServer +) -> None: + """Locally, a denied Apify Proxy access yields no proxy configuration.""" + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') + monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) + httpserver.expect_oneshot_request('/').respond_with_json( + {'connected': False, 'connectionError': 'DUMMY_CONNECTION_ERROR'}, + status=200, + ) + + async with Actor: + proxy_configuration = await Actor.create_proxy_configuration(password=DUMMY_PASSWORD) + + assert proxy_configuration is None diff --git a/tests/unit/test_proxy_configuration.py b/tests/unit/test_proxy_configuration.py index 5093505fc..63e52ce83 100644 --- a/tests/unit/test_proxy_configuration.py +++ b/tests/unit/test_proxy_configuration.py @@ -548,9 +548,35 @@ async def test_initialize_with_manual_password_different_than_user_one( @pytest.mark.usefixtures('patched_impit_client') -async def test_initialize_when_not_connected(monkeypatch: pytest.MonkeyPatch, httpserver: HTTPServer) -> None: +async def test_initialize_when_not_connected( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, httpserver: HTTPServer +) -> None: + """Locally, a denied proxy access logs the connection error and reports no usable proxy.""" + caplog.set_level('WARNING') + dummy_connection_error = 'DUMMY_CONNECTION_ERROR' + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') + monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) + + httpserver.expect_oneshot_request('/').respond_with_json( + { + 'connected': False, + 'connectionError': dummy_connection_error, + }, + status=200, + ) + + proxy_configuration = ProxyConfiguration(password=DUMMY_PASSWORD) + + assert await proxy_configuration.initialize() is False + assert any(dummy_connection_error in msg for msg in caplog.messages) + + +@pytest.mark.usefixtures('patched_impit_client') +async def test_initialize_when_not_connected_at_home(monkeypatch: pytest.MonkeyPatch, httpserver: HTTPServer) -> None: + """On the platform, a denied proxy access raises a `ConnectionError`.""" dummy_connection_error = 'DUMMY_CONNECTION_ERROR' dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') + monkeypatch.setenv(ApifyEnvVars.IS_AT_HOME.value, '1') monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) httpserver.expect_oneshot_request('/').respond_with_json( @@ -567,6 +593,52 @@ async def test_initialize_when_not_connected(monkeypatch: pytest.MonkeyPatch, ht await proxy_configuration.initialize() +async def test_initialize_when_password_fetch_fails( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + httpserver: HTTPServer, + apify_client_async_patcher: ApifyClientAsyncPatcher, +) -> None: + """Locally, a failed password fetch logs a warning and reports no usable proxy.""" + caplog.set_level('WARNING') + monkeypatch.delenv(ApifyEnvVars.PROXY_PASSWORD.value, raising=False) + monkeypatch.setenv(ApifyEnvVars.TOKEN.value, 'DUMMY_TOKEN') + dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/') + monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url) + apify_client_async_patcher.patch('user', 'get', replacement_method=Mock(side_effect=RuntimeError('Invalid token'))) + + proxy_configuration = ProxyConfiguration(_apify_client=ApifyClientAsync()) + + assert await proxy_configuration.initialize() is False + assert 'Failed to fetch the Apify Proxy password using the Apify token.' in caplog.messages + assert len(httpserver.log) == 0 + + +async def test_initialize_when_password_fetch_fails_at_home( + monkeypatch: pytest.MonkeyPatch, apify_client_async_patcher: ApifyClientAsyncPatcher +) -> None: + """On the platform, a failed password fetch raises the client error.""" + monkeypatch.setenv(ApifyEnvVars.IS_AT_HOME.value, '1') + monkeypatch.delenv(ApifyEnvVars.PROXY_PASSWORD.value, raising=False) + monkeypatch.setenv(ApifyEnvVars.TOKEN.value, 'DUMMY_TOKEN') + apify_client_async_patcher.patch('user', 'get', replacement_method=Mock(side_effect=RuntimeError('Invalid token'))) + + proxy_configuration = ProxyConfiguration(_apify_client=ApifyClientAsync()) + + with pytest.raises(RuntimeError, match='Invalid token'): + await proxy_configuration.initialize() + + +async def test_new_url_without_password(monkeypatch: pytest.MonkeyPatch) -> None: + """Without a password, Apify Proxy yields no URL and no proxy info.""" + monkeypatch.delenv(ApifyEnvVars.PROXY_PASSWORD.value, raising=False) + + proxy_configuration = ProxyConfiguration() + + assert await proxy_configuration.new_url() is None + assert await proxy_configuration.new_proxy_info() is None + + async def test_initialize_when_status_page_unavailable( monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, httpserver: HTTPServer ) -> None: From 59a0de4da805eb780bc8e6743e4ab2e71d729f3b Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 10 Nov 2026 10:30:58 +0100 Subject: [PATCH 09/12] fix: create the Scrapy proxy configuration once for concurrent requests --- src/apify/scrapy/middlewares/apify_proxy.py | 37 ++++++++----------- .../scrapy/middlewares/test_apify_proxy.py | 18 +++++++++ 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/apify/scrapy/middlewares/apify_proxy.py b/src/apify/scrapy/middlewares/apify_proxy.py index cd0420ab2..2cf0a7368 100644 --- a/src/apify/scrapy/middlewares/apify_proxy.py +++ b/src/apify/scrapy/middlewares/apify_proxy.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from typing import TYPE_CHECKING from urllib.parse import ParseResult, urlparse @@ -33,6 +34,7 @@ def __init__(self, proxy_settings: dict) -> None: """ self._proxy_settings = proxy_settings self._proxy_cfg_internal: ProxyConfiguration | None = None + self._proxy_cfg_lock = asyncio.Lock() self._runs_without_proxy = False @classmethod @@ -67,7 +69,7 @@ def from_crawler(cls: type[ApifyHttpProxyMiddleware], crawler: Crawler) -> Apify async def process_request(self, request: Request) -> None: """Process a Scrapy request by assigning a new proxy. - In a local run without Apify Proxy credentials, the request is left without a proxy. + In a local run that cannot use Apify Proxy, the request is left without a proxy. Args: request: Scrapy Request object. @@ -115,25 +117,18 @@ async def _get_new_proxy_url(self) -> ParseResult | None: Returns: New proxy URL, or None when the run goes without a proxy. """ - if self._runs_without_proxy: + # The lock makes concurrent first requests create the proxy configuration only once. + async with self._proxy_cfg_lock: + if self._proxy_cfg_internal is None and not self._runs_without_proxy: + self._proxy_cfg_internal = await Actor.create_proxy_configuration( + actor_proxy_input=self._proxy_settings + ) + # `from_crawler` guarantees `useApifyProxy` is set, so None only comes from a local run that cannot + # use Apify Proxy. `ProxyConfiguration.initialize` has already logged a warning about it. + self._runs_without_proxy = self._proxy_cfg_internal is None + + if self._proxy_cfg_internal is None: return None - # Get proxy configuration, creating it if necessary - proxy_cfg = ( - self._proxy_cfg_internal - if isinstance(self._proxy_cfg_internal, ProxyConfiguration) - else await Actor.create_proxy_configuration(actor_proxy_input=self._proxy_settings) - ) - - # `from_crawler` guarantees `useApifyProxy` is set, so None only comes from a local run without Apify Proxy - # credentials. `ProxyConfiguration.initialize` has already logged a warning about it. - if proxy_cfg is None: - self._runs_without_proxy = True - return None - - # Store the proxy configuration for future use - self._proxy_cfg_internal = proxy_cfg - - # Get a new proxy URL and return it - new_url = await proxy_cfg.new_url() - return urlparse(str(new_url)) + new_url = await self._proxy_cfg_internal.new_url() + return urlparse(new_url) if new_url else None diff --git a/tests/unit/scrapy/middlewares/test_apify_proxy.py b/tests/unit/scrapy/middlewares/test_apify_proxy.py index 528b314e5..fa4fffae1 100644 --- a/tests/unit/scrapy/middlewares/test_apify_proxy.py +++ b/tests/unit/scrapy/middlewares/test_apify_proxy.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from unittest.mock import AsyncMock from urllib.parse import ParseResult, urlparse @@ -151,6 +152,23 @@ async def test_process_request_without_proxy_configuration( create_proxy_configuration.assert_awaited_once() +async def test_concurrent_requests_create_proxy_configuration_once( + monkeypatch: pytest.MonkeyPatch, + middleware: ApifyHttpProxyMiddleware, +) -> None: + """Concurrent first requests share one proxy configuration creation.""" + + async def slow_create_proxy_configuration(**_: object) -> None: + await asyncio.sleep(0.01) + + create_proxy_configuration = AsyncMock(side_effect=slow_create_proxy_configuration) + monkeypatch.setattr(Actor, 'create_proxy_configuration', create_proxy_configuration) + + await asyncio.gather(*(middleware.process_request(Request(f'https://example.com/{i}')) for i in range(3))) + + create_proxy_configuration.assert_awaited_once() + + @pytest.mark.parametrize( 'exception', [TunnelError(), ValueError()], From e438358bcb8e02a6dea12d622064f983dc6b004a Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 10 Nov 2026 10:30:58 +0100 Subject: [PATCH 10/12] docs: describe the local run without Apify Proxy access --- docs/02_concepts/05_proxy_management.mdx | 2 +- docs/02_concepts/13_exceptions.mdx | 2 +- docs/02_concepts/code/05_apify_proxy.py | 3 ++- docs/02_concepts/code/05_apify_proxy_config.py | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/02_concepts/05_proxy_management.mdx b/docs/02_concepts/05_proxy_management.mdx index 8fb036ed0..f4ead4b1b 100644 --- a/docs/02_concepts/05_proxy_management.mdx +++ b/docs/02_concepts/05_proxy_management.mdx @@ -20,7 +20,7 @@ The Apify SDK provides built-in proxy management through the `Actor.create_proxy_configuration()` logs a warning and returns `None`, and the Actor runs without a proxy. +If you want to use Apify Proxy locally, make sure that you run your Actors via the Apify CLI and that you are [logged in](https://docs.apify.com/cli/docs/installation#login-with-your-apify-account) with your Apify account in the CLI. In a local run without Apify Proxy credentials, or when Apify Proxy denies access, `Actor.create_proxy_configuration()` logs a warning and returns `None`, and the Actor runs without a proxy. ### Using Apify Proxy diff --git a/docs/02_concepts/13_exceptions.mdx b/docs/02_concepts/13_exceptions.mdx index 2bec78ed7..7fd54b47d 100644 --- a/docs/02_concepts/13_exceptions.mdx +++ b/docs/02_concepts/13_exceptions.mdx @@ -43,7 +43,7 @@ The SDK raises standard Python exceptions when it's used incorrectly or given in - [`RuntimeError`](https://docs.python.org/3/library/exceptions.html#RuntimeError) when an `Actor` method is used outside the `async with Actor:` block, either before initialization or after exit, or when the Actor is initialized twice. - [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError) for an invalid argument, such as a malformed `timeout`, an invalid proxy configuration, charging an automatically charged event by hand, or pushing data that is not JSON-serializable or is over the size limit. - [`TypeError`](https://docs.python.org/3/library/exceptions.html#TypeError) for an argument of the wrong type. -- [`ConnectionError`](https://docs.python.org/3/library/exceptions.html#ConnectionError) when `Actor.create_proxy_configuration` verifies Apify Proxy access and the proxy reports that you have none. +- [`ConnectionError`](https://docs.python.org/3/library/exceptions.html#ConnectionError) when `Actor.create_proxy_configuration` verifies Apify Proxy access on the Apify platform and the proxy reports that you have none. In a local run, the method logs a warning and returns `None` instead. ## Run failures diff --git a/docs/02_concepts/code/05_apify_proxy.py b/docs/02_concepts/code/05_apify_proxy.py index 12d1dcca2..902ec1f74 100644 --- a/docs/02_concepts/code/05_apify_proxy.py +++ b/docs/02_concepts/code/05_apify_proxy.py @@ -8,7 +8,8 @@ async def main() -> None: proxy_cfg = await Actor.create_proxy_configuration() if not proxy_cfg: - raise RuntimeError('No proxy configuration available.') + Actor.log.warning('Apify Proxy is not available, exiting...') + return proxy_url = await proxy_cfg.new_url() Actor.log.info(f'Using proxy URL: {proxy_url}') diff --git a/docs/02_concepts/code/05_apify_proxy_config.py b/docs/02_concepts/code/05_apify_proxy_config.py index 40dac7387..948c0fd79 100644 --- a/docs/02_concepts/code/05_apify_proxy_config.py +++ b/docs/02_concepts/code/05_apify_proxy_config.py @@ -12,7 +12,8 @@ async def main() -> None: ) if not proxy_cfg: - raise RuntimeError('No proxy configuration available.') + Actor.log.warning('Apify Proxy is not available, exiting...') + return proxy_url = await proxy_cfg.new_url() Actor.log.info(f'Proxy URL: {proxy_url}') From a2ba1677d6c9901a7adf54a645012eabfefc4250 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 10 Nov 2026 11:15:16 +0100 Subject: [PATCH 11/12] chore(deps): update crawlee to 1.11.2 --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 83eee90ea..111d5ea8c 100644 --- a/uv.lock +++ b/uv.lock @@ -998,7 +998,7 @@ wheels = [ [[package]] name = "crawlee" -version = "1.11.1" +version = "1.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout" }, @@ -1014,9 +1014,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/36/73371dda02f1b992171f1bcd8e0afa192c2dda29314d86c0618023608193/crawlee-1.11.1.tar.gz", hash = "sha256:7b504f8c63957a7fd2eacfaa14cbeb82b834f2e172fdcf3410349f42de46943b", size = 408277, upload-time = "2026-11-05T14:43:28.784Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/e7/3cf4c31a8595f554afeb5b6819d2cbd93e682bfccb34b2ff77d38227026c/crawlee-1.11.2.tar.gz", hash = "sha256:c87401f1378fe539afe4d2637a3a3fee659ea9bfcfab6495b0ea7494e5eeb4c2", size = 409471, upload-time = "2026-11-10T10:13:52.695Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/a5/c000be053571f40a9dbddfdb7f890144c5fbd9deec256026ad242f4e6ebf/crawlee-1.11.1-py3-none-any.whl", hash = "sha256:125ed497e4607e5e58c22e58c089e5d5bedc3b7392d601923b83bfe0ca722b79", size = 474625, upload-time = "2026-11-05T14:43:26.831Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8c/710cb7beaac61172732f4ad42f3061fba1534ba2fe35048b586ecf8849e1/crawlee-1.11.2-py3-none-any.whl", hash = "sha256:be6bd8f555dac1894812d5473cc2aee3c86e6c8bea9395c717320a6a89e6e0cb", size = 474838, upload-time = "2026-11-10T10:13:50.881Z" }, ] [package.optional-dependencies] From eb576e9f9eeb26dd5c50425febc82b092d987935 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 10 Nov 2026 11:15:16 +0100 Subject: [PATCH 12/12] docs: pass the optional proxy configuration to the Crawlee crawlers directly --- .../code/05_crawlee_beautifulsoup.py | 23 +++++++------------ docs/03_guides/code/05_crawlee_parsel.py | 19 +++++++-------- docs/03_guides/code/05_crawlee_playwright.py | 21 +++++------------ 3 files changed, 22 insertions(+), 41 deletions(-) diff --git a/docs/03_guides/code/05_crawlee_beautifulsoup.py b/docs/03_guides/code/05_crawlee_beautifulsoup.py index 91ca173a1..c7cadd8a8 100644 --- a/docs/03_guides/code/05_crawlee_beautifulsoup.py +++ b/docs/03_guides/code/05_crawlee_beautifulsoup.py @@ -1,10 +1,6 @@ import asyncio -from crawlee.crawlers import ( - BeautifulSoupCrawler, - BeautifulSoupCrawlingContext, - HttpCrawlerOptions, -) +from crawlee.crawlers import BeautifulSoupCrawler, BeautifulSoupCrawlingContext from crawlee.router import Router from apify import Actor @@ -46,19 +42,16 @@ async def main() -> None: Actor.log.info('No start URLs specified in Actor input, exiting...') await Actor.exit() - crawler_options: HttpCrawlerOptions[BeautifulSoupCrawlingContext] = { - 'request_handler': router, - # Cap the crawl. Remove or increase the limit to follow all links. - 'max_requests_per_crawl': 10, - } - # Crawlee rotates the proxy URL per request on its own. A local run without - # Apify Proxy credentials gets no proxy configuration and crawls directly. + # Apify Proxy access gets no proxy configuration and crawls directly. proxy_configuration = await Actor.create_proxy_configuration() - if proxy_configuration is not None: - crawler_options['proxy_configuration'] = proxy_configuration - crawler = BeautifulSoupCrawler(**crawler_options) + crawler = BeautifulSoupCrawler( + proxy_configuration=proxy_configuration, + request_handler=router, + # Cap the crawl. Remove or increase the limit to follow all links. + max_requests_per_crawl=10, + ) await crawler.run(start_urls) diff --git a/docs/03_guides/code/05_crawlee_parsel.py b/docs/03_guides/code/05_crawlee_parsel.py index 511a97958..4f88fe852 100644 --- a/docs/03_guides/code/05_crawlee_parsel.py +++ b/docs/03_guides/code/05_crawlee_parsel.py @@ -1,6 +1,6 @@ import asyncio -from crawlee.crawlers import HttpCrawlerOptions, ParselCrawler, ParselCrawlingContext +from crawlee.crawlers import ParselCrawler, ParselCrawlingContext from crawlee.router import Router from apify import Actor @@ -42,19 +42,16 @@ async def main() -> None: Actor.log.info('No start URLs specified in Actor input, exiting...') await Actor.exit() - crawler_options: HttpCrawlerOptions[ParselCrawlingContext] = { - 'request_handler': router, - # Cap the crawl. Remove or increase the limit to follow all links. - 'max_requests_per_crawl': 10, - } - # Crawlee rotates the proxy URL per request on its own. A local run without - # Apify Proxy credentials gets no proxy configuration and crawls directly. + # Apify Proxy access gets no proxy configuration and crawls directly. proxy_configuration = await Actor.create_proxy_configuration() - if proxy_configuration is not None: - crawler_options['proxy_configuration'] = proxy_configuration - crawler = ParselCrawler(**crawler_options) + crawler = ParselCrawler( + proxy_configuration=proxy_configuration, + request_handler=router, + # Cap the crawl. Remove or increase the limit to follow all links. + max_requests_per_crawl=10, + ) await crawler.run(start_urls) diff --git a/docs/03_guides/code/05_crawlee_playwright.py b/docs/03_guides/code/05_crawlee_playwright.py index eb714badd..1bd7ddf90 100644 --- a/docs/03_guides/code/05_crawlee_playwright.py +++ b/docs/03_guides/code/05_crawlee_playwright.py @@ -1,10 +1,6 @@ import asyncio -from crawlee.crawlers import ( - BasicCrawlerOptions, - PlaywrightCrawler, - PlaywrightCrawlingContext, -) +from crawlee.crawlers import PlaywrightCrawler, PlaywrightCrawlingContext from crawlee.router import Router from apify import Actor @@ -46,25 +42,20 @@ async def main() -> None: Actor.log.info('No start URLs specified in Actor input, exiting...') await Actor.exit() - crawler_options: BasicCrawlerOptions[PlaywrightCrawlingContext] = { - 'request_handler': router, - # Cap the crawl. Remove or increase the limit to follow all links. - 'max_requests_per_crawl': 10, - } - # Crawlee rotates the proxy URL per request on its own. A local run without - # Apify Proxy credentials gets no proxy configuration and crawls directly. + # Apify Proxy access gets no proxy configuration and crawls directly. proxy_configuration = await Actor.create_proxy_configuration() - if proxy_configuration is not None: - crawler_options['proxy_configuration'] = proxy_configuration # Common Chrome flags for running the browser in a container. browser_args = ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'] crawler = PlaywrightCrawler( + proxy_configuration=proxy_configuration, + request_handler=router, + # Cap the crawl. Remove or increase the limit to follow all links. + max_requests_per_crawl=10, headless=True, browser_launch_options={'args': browser_args}, - **crawler_options, ) await crawler.run(start_urls)