@@ -204,6 +204,12 @@ class _BasicCrawlerOptions(TypedDict):
204204 """If set to `True`, the crawler will automatically try to fetch the robots.txt file for each domain,
205205 and skip those that are not allowed. This also prevents disallowed URLs to be added via `EnqueueLinksFunction`."""
206206
207+ send_request_enqueue_strategy : NotRequired [EnqueueStrategy ]
208+ """Strategy applied by `BasicCrawlingContext.send_request` to validate the target URL against the current request's
209+ URL. Defaults to `'all'`, which preserves the historical behaviour of allowing arbitrary URLs. Set to e.g.
210+ `'same-hostname'` to restrict `send_request` to the same host as the current request, for example when handlers
211+ pass URLs extracted from page content to `send_request` directly."""
212+
207213 status_message_logging_interval : NotRequired [timedelta ]
208214 """Interval for logging the crawler status messages."""
209215
@@ -299,6 +305,7 @@ def __init__(
299305 configure_logging : bool = True ,
300306 statistics_log_format : Literal ['table' , 'inline' ] = 'table' ,
301307 respect_robots_txt_file : bool = False ,
308+ send_request_enqueue_strategy : EnqueueStrategy = 'all' ,
302309 status_message_logging_interval : timedelta = timedelta (seconds = 10 ),
303310 status_message_callback : Callable [[StatisticsState , StatisticsState | None , str ], Awaitable [str | None ]]
304311 | None = None ,
@@ -352,6 +359,11 @@ def __init__(
352359 respect_robots_txt_file: If set to `True`, the crawler will automatically try to fetch the robots.txt file
353360 for each domain, and skip those that are not allowed. This also prevents disallowed URLs to be added
354361 via `EnqueueLinksFunction`
362+ send_request_enqueue_strategy: Strategy applied by `BasicCrawlingContext.send_request` to validate the
363+ target URL against the current request's URL (`loaded_url` if available, otherwise `url`). Defaults
364+ to `'all'`, preserving the historical behaviour of allowing arbitrary URLs. Set to e.g.
365+ `'same-hostname'` when handlers extract URLs from page content and pass them straight to
366+ `send_request`, to keep cross-host fetches intentional.
355367 status_message_logging_interval: Interval for logging the crawler status messages.
356368 status_message_callback: Allows overriding the default status message. The default status message is
357369 provided in the parameters. Returning `None` suppresses the status message.
@@ -432,6 +444,7 @@ def __init__(
432444 self ._max_session_rotations = max_session_rotations
433445 self ._max_crawl_depth = max_crawl_depth
434446 self ._respect_robots_txt_file = respect_robots_txt_file
447+ self ._send_request_enqueue_strategy = send_request_enqueue_strategy
435448
436449 # Timeouts
437450 self ._request_handler_timeout = request_handler_timeout
@@ -1275,14 +1288,28 @@ def _prepare_send_request_function(
12751288 self ,
12761289 session : Session | None ,
12771290 proxy_info : ProxyInfo | None ,
1291+ request : Request ,
12781292 ) -> SendRequestFunction :
1293+ strategy = self ._send_request_enqueue_strategy
1294+ origin_url = request .loaded_url or request .url
1295+ origin_parsed = urlparse (origin_url ) if strategy != 'all' else None
1296+
12791297 async def send_request (
12801298 url : str ,
12811299 * ,
12821300 method : HttpMethod = 'GET' ,
12831301 payload : HttpPayload | None = None ,
12841302 headers : HttpHeaders | dict [str , str ] | None = None ,
12851303 ) -> HttpResponse :
1304+ if origin_parsed is not None and not self ._check_enqueue_strategy (
1305+ strategy ,
1306+ target_url = urlparse (url ),
1307+ origin_url = origin_parsed ,
1308+ ):
1309+ raise ValueError (
1310+ f'send_request() refusing to fetch { url !r} : does not match enqueue strategy '
1311+ f'{ strategy !r} relative to { origin_url !r} .'
1312+ )
12861313 return await self ._http_client .send_request (
12871314 url = url ,
12881315 method = method ,
@@ -1428,7 +1455,7 @@ async def __run_task_function(self) -> None:
14281455 request = result .request ,
14291456 session = session ,
14301457 proxy_info = proxy_info ,
1431- send_request = self ._prepare_send_request_function (session , proxy_info ),
1458+ send_request = self ._prepare_send_request_function (session , proxy_info , request ),
14321459 add_requests = result .add_requests ,
14331460 push_data = result .push_data ,
14341461 get_key_value_store = result .get_key_value_store ,
0 commit comments