11import type { ProxyConfigurationOptions } from 'apify' ;
22import { Actor } from 'apify' ;
3- import type { ProxyConfiguration } from 'crawlee' ;
4- import { BrowserName , log } from 'crawlee' ;
3+ import type { CheerioCrawlerOptions } from 'crawlee' ;
4+ import { BrowserName , log , ProxyConfiguration } from 'crawlee' ;
55import { firefox } from 'playwright' ;
66
77import ragWebBrowserInputSchema from '../actors/apify_rag-web-browser/.actor/input_schema.json' with { type : 'json' } ;
@@ -11,17 +11,17 @@ import { blockMediaRequests } from './media.js';
1111import { getMiniActor } from './mini-actors.js' ;
1212import type {
1313 ContentCrawlerOptions ,
14+ ContentCrawlerUserData ,
1415 ContentScraperSettings ,
1516 Input ,
1617 OutputFormats ,
1718 ProxyOptions ,
1819 RagWebBrowserInput ,
1920 ScrapingTool ,
20- SearchCrawlerOptions ,
2121 SERPProxyGroup ,
2222 UrlToMarkdownInput ,
2323} from './types.js' ;
24- import { abortRun } from './utils.js' ;
24+ import { abortRun , isActorStandby } from './utils.js' ;
2525
2626const proxyConfigurations = new Map < string , Promise < ProxyConfiguration | undefined > > ( ) ;
2727
@@ -44,11 +44,43 @@ async function getProxyConfiguration(proxyOptions: ProxyOptions) {
4444 return configuration ;
4545}
4646
47- async function createContentProxyConfiguration ( proxyOptions : ProxyOptions ) {
47+ const DEFAULT_PROXY_OPTIONS = ragWebBrowserInputSchema . properties . proxyConfiguration . default as ProxyOptions ;
48+
49+ /**
50+ * The configuration every crawler is built with. It resolves the proxy from the request Crawlee is
51+ * about to send rather than from the input the crawler was built with, so one crawler serves callers
52+ * that each asked for a different proxy - which is what keeps the Standby crawler set at three
53+ * however many callers there are.
54+ *
55+ * Crawlee calls this per request for the HTTP crawlers and per page for the browser one. The browser
56+ * one only honours it because its launch context sets `useIncognitoPages`; without that, BrowserPool
57+ * hands every request whichever browser is free and they all go through the first browser's proxy.
58+ */
59+ const requestProxyConfiguration = new ProxyConfiguration ( {
60+ newUrlFunction : async ( sessionId , options ) => {
61+ // Crawlee also resolves a proxy when it launches a browser, with no request in hand.
62+ const userData = options ?. request ?. userData as Partial < ContentCrawlerUserData > | undefined ;
63+ const configuration = await getProxyConfiguration ( userData ?. proxyOptions ?? DEFAULT_PROXY_OPTIONS ) ;
64+
65+ // Crawlee reads this off the configuration, right after this call, to decide whether to
66+ // ignore TLS errors. Keep it in step with whichever configuration served the request.
67+ requestProxyConfiguration . isManInTheMiddle = configuration ?. isManInTheMiddle ?? false ;
68+
69+ return ( await configuration ?. newUrl ( sessionId ) ) ?? null ;
70+ } ,
71+ } ) ;
72+
73+ /**
74+ * Builds the caller's proxy configuration so that an unusable one is reported now, rather than as a
75+ * run of failed pages later. The crawlers themselves route through `requestProxyConfiguration`.
76+ */
77+ async function validateProxyConfiguration ( proxyOptions : ProxyOptions , abortOnFailure : boolean ) {
4878 try {
49- return await getProxyConfiguration ( proxyOptions ) ;
79+ await getProxyConfiguration ( proxyOptions ) ;
5080 } catch ( e ) {
51- return abortRun ( `Cannot use Apify Proxy for scraping the target pages: ${ ( e as Error ) . message } ` ) ;
81+ const message = `Cannot use Apify Proxy for scraping the target pages: ${ ( e as Error ) . message } ` ;
82+ if ( ! abortOnFailure ) throw new Error ( message ) ;
83+ await abortRun ( message ) ;
5284 }
5385}
5486
@@ -59,10 +91,10 @@ async function createContentProxyConfiguration(proxyOptions: ProxyOptions) {
5991export async function processStandbyInput ( originalInput : Partial < Input > ) {
6092 const { input, searchCrawlerOptions, contentScraperSettings } = await processInputInternal ( originalInput , true ) ;
6193
62- const proxy = await createContentProxyConfiguration ( input . proxyConfiguration ) ;
94+ await validateProxyConfiguration ( input . proxyConfiguration , true ) ;
6395 const contentCrawlerOptions : ContentCrawlerOptions [ ] = [
64- createPlaywrightCrawlerOptions ( input , proxy ) ,
65- createCheerioCrawlerOptions ( input , proxy ) ,
96+ createPlaywrightCrawlerOptions ( input ) ,
97+ createCheerioCrawlerOptions ( input ) ,
6698 ] ;
6799
68100 return { input, searchCrawlerOptions, contentCrawlerOptions, contentScraperSettings } ;
@@ -74,10 +106,12 @@ export async function processStandbyInput(originalInput: Partial<Input>) {
74106export async function processInput ( originalInput : Partial < Input > ) {
75107 const { input, searchCrawlerOptions, contentScraperSettings } = await processInputInternal ( originalInput ) ;
76108
77- const proxy = await createContentProxyConfiguration ( input . proxyConfiguration ) ;
109+ // In Standby this runs once per request, where a proxy problem must fail that request alone. In
110+ // Normal mode it is the run's startup, where there is nothing to serve and the run ends.
111+ await validateProxyConfiguration ( input . proxyConfiguration , ! isActorStandby ( ) ) ;
78112 const contentCrawlerOptions : ContentCrawlerOptions = input . scrapingTool === 'raw-http'
79- ? createCheerioCrawlerOptions ( input , proxy , false )
80- : createPlaywrightCrawlerOptions ( input , proxy , false ) ;
113+ ? createCheerioCrawlerOptions ( input , false )
114+ : createPlaywrightCrawlerOptions ( input , false ) ;
81115
82116 return { input, searchCrawlerOptions, contentCrawlerOptions, contentScraperSettings } ;
83117}
@@ -91,7 +125,7 @@ async function processInputInternal(
91125) {
92126 const miniActor = getMiniActor ( ) ;
93127 let input : Input ;
94- let searchCrawlerOptions : SearchCrawlerOptions = { crawlerOptions : { } , proxyOptions : { } } ;
128+ let searchCrawlerOptions : CheerioCrawlerOptions = { } ;
95129
96130 if ( miniActor . runsSearch ) {
97131 const processedRagWebBrowserInput = await processRagWebBrowserInput (
@@ -130,7 +164,7 @@ async function processInputInternal(
130164async function processRagWebBrowserInput ( input : Partial < RagWebBrowserInput > , standbyInit : boolean ) :
131165 Promise < {
132166 validatedRagBrowserInput : RagWebBrowserInput ;
133- searchCrawlerOptions : SearchCrawlerOptions
167+ searchCrawlerOptions : CheerioCrawlerOptions
134168 } > {
135169 /* eslint-disable no-param-reassign */
136170
@@ -199,15 +233,11 @@ async function processRagWebBrowserInput(input: Partial<RagWebBrowserInput>, sta
199233 input . dynamicContentWaitSecs = Math . round ( input . requestTimeoutSecs / 2 ) ;
200234 }
201235
202- const proxyOptions : ProxyOptions = { groups : [ input . serpProxyGroup ] , checkAccess : false } ;
203- const searchCrawlerOptions : SearchCrawlerOptions = {
204- crawlerOptions : {
205- keepAlive : standbyInit ,
206- maxRequestRetries : CRAWLER_MAX_REQUEST_RETRIES ,
207- proxyConfiguration : await getProxyConfiguration ( proxyOptions ) ,
208- autoscaledPoolOptions : { desiredConcurrency : 1 } ,
209- } ,
210- proxyOptions,
236+ const searchCrawlerOptions : CheerioCrawlerOptions = {
237+ keepAlive : standbyInit ,
238+ maxRequestRetries : CRAWLER_MAX_REQUEST_RETRIES ,
239+ proxyConfiguration : requestProxyConfiguration ,
240+ autoscaledPoolOptions : { desiredConcurrency : 1 } ,
211241 } ;
212242 const validatedRagBrowserInput = validateAndFillInput ( input ) as RagWebBrowserInput ;
213243 return {
@@ -248,20 +278,27 @@ async function processUrlToMarkdownInput(input: Partial<UrlToMarkdownInput>): Pr
248278
249279function createPlaywrightCrawlerOptions (
250280 input : Input ,
251- proxy : ProxyConfiguration | undefined ,
252281 keepAlive = true ,
253282) : ContentCrawlerOptions {
254283 return {
255284 type : ContentCrawlerTypes . PLAYWRIGHT ,
256- proxyOptions : input . proxyConfiguration ,
257285 crawlerOptions : {
258286 headless : true ,
259287 keepAlive,
288+ // The session pool is shared by every caller, so a session's cookies and its Apify Proxy
289+ // session id would be reused across requests that went out through different proxies -
290+ // one caller's cookies riding another's IP. A request here is one page, which gains
291+ // nothing from a session, so there is none.
292+ useSessionPool : false ,
260293 maxRequestRetries : CRAWLER_MAX_REQUEST_RETRIES ,
261- proxyConfiguration : proxy ,
294+ proxyConfiguration : requestProxyConfiguration ,
262295 requestHandlerTimeoutSecs : CRAWLER_REQUEST_HANDLER_TIMEOUT_SECS ,
263296 launchContext : {
264297 launcher : firefox ,
298+ // Gives each page its own browser context, which is what lets a page carry the proxy
299+ // its request asked for. Without it BrowserPool reuses whichever browser is free and
300+ // every request goes through the proxy the first one launched with - silently.
301+ useIncognitoPages : true ,
265302 } ,
266303 preNavigationHooks : [
267304 async ( { page } ) => {
@@ -289,17 +326,16 @@ function createPlaywrightCrawlerOptions(
289326
290327function createCheerioCrawlerOptions (
291328 input : Input ,
292- proxy : ProxyConfiguration | undefined ,
293329 keepAlive = true ,
294330) : ContentCrawlerOptions {
295331 return {
296332 type : ContentCrawlerTypes . CHEERIO ,
297- proxyOptions : input . proxyConfiguration ,
298333 crawlerOptions : {
299334 keepAlive,
300335 maxRequestRetries : CRAWLER_MAX_REQUEST_RETRIES ,
301- proxyConfiguration : proxy ,
336+ proxyConfiguration : requestProxyConfiguration ,
302337 requestHandlerTimeoutSecs : CRAWLER_REQUEST_HANDLER_TIMEOUT_SECS ,
338+ useSessionPool : false ,
303339 autoscaledPoolOptions : {
304340 desiredConcurrency : input . desiredConcurrency ,
305341 } ,
0 commit comments