@@ -2,6 +2,7 @@ import type {
22 Awaitable ,
33 BasicCrawlerOptions ,
44 BasicCrawlingContext ,
5+ ContextMiddleware ,
56 CrawlingContext ,
67 Dictionary ,
78 EnqueueLinksOptions ,
@@ -58,6 +59,7 @@ export interface BrowserCrawlingContext<
5859 Response extends BaseResponse = BaseResponse ,
5960 ProvidedController = BrowserController ,
6061 UserData extends Dictionary = Dictionary ,
62+ GoToOptions extends Dictionary = Dictionary ,
6163> extends CrawlingContext < UserData > {
6264 /**
6365 * An instance of the {@apilink BrowserController} that manages the browser instance and provides access to its API.
@@ -79,16 +81,25 @@ export interface BrowserCrawlingContext<
7981 */
8082 response : Response ;
8183
84+ /**
85+ * Options object passed to the underlying `page.goto()` call. `preNavigationHooks` can mutate this
86+ * object (or return `{ gotoOptions: ... }`) to influence the navigation.
87+ */
88+ gotoOptions : GoToOptions ;
89+
8290 /**
8391 * Helper function for extracting URLs from the current page and adding them to the request queue.
8492 */
8593 enqueueLinks : ( options ?: EnqueueLinksOptions ) => Promise < BatchAddRequestsResult > ;
8694}
8795
88- export type BrowserHook < Context = BrowserCrawlingContext , GoToOptions extends Dictionary | undefined = Dictionary > = (
96+ export type BrowserHook < Context = BrowserCrawlingContext > = (
8997 crawlingContext : Context ,
90- gotoOptions : GoToOptions ,
91- ) => Awaitable < void > ;
98+ ) => Awaitable < void | Partial < Context > > ;
99+
100+ const COOKIES_BEFORE_HOOKS = Symbol ( 'cookiesBeforeHooks' ) ;
101+
102+ const readContextField = < T > ( ctx : object , key : symbol ) : T => ( ctx as Record < symbol , unknown > ) [ key ] as T ;
92103
93104export interface BrowserCrawlerOptions <
94105 Page extends CommonPage = CommonPage ,
@@ -174,31 +185,33 @@ export interface BrowserCrawlerOptions<
174185
175186 /**
176187 * Async functions that are sequentially evaluated before the navigation. Good for setting additional cookies
177- * or browser properties before navigation. The function accepts two parameters, `crawlingContext` and `gotoOptions`,
178- * which are passed to the `page.goto()` function the crawler calls to navigate .
188+ * or browser properties before navigation. The function receives the `crawlingContext`; the options object
189+ * forwarded to `page.goto()` is available as `crawlingContext.gotoOptions` and can be mutated in place .
179190 *
180191 * **Example:**
181192 *
182193 * ```js
183194 * preNavigationHooks: [
184- * async (crawlingContext, gotoOptions) => {
185- * const { page } = crawlingContext;
195+ * async ({ page, gotoOptions }) => {
186196 * await page.evaluate((attr) => { window.foo = attr; }, 'bar');
187197 * gotoOptions.timeout = 60_000;
188198 * gotoOptions.waitUntil = 'domcontentloaded';
189199 * },
190200 * ]
191201 * ```
192202 *
193- * Modyfing `pageOptions` is supported only in Playwright incognito.
194- * See { @apilink PrePageCreateHook}
203+ * A hook may optionally return a partial object whose properties are merged into the crawling context,
204+ * allowing the hook to override context members for subsequent hooks and pipeline stages.
195205 */
196206 preNavigationHooks ?: BrowserHook < Context > [ ] ;
197207
198208 /**
199209 * Async functions that are sequentially evaluated after the navigation. Good for checking if the navigation was successful.
200210 * The function accepts `crawlingContext` as the only parameter.
201211 *
212+ * A hook may optionally return a partial object whose properties are merged into the crawling context.
213+ * This is useful for overriding context members (e.g. `response`) after solving a challenge.
214+ *
202215 * **Example:**
203216 *
204217 * ```js
@@ -209,6 +222,11 @@ export interface BrowserCrawlerOptions<
209222 * await solveCaptcha(page);
210223 * }
211224 * },
225+ * async (crawlingContext) => {
226+ * if (await needsRevalidation(crawlingContext)) {
227+ * return { response: await crawlingContext.page.reload() };
228+ * }
229+ * },
212230 * ]
213231 * ```
214232 */
@@ -358,13 +376,32 @@ export abstract class BrowserCrawler<
358376 ...basicCrawlerOptions
359377 } = options ;
360378
379+ const skipGuard = < Ctx extends Context > (
380+ action : ( ctx : Ctx ) => Awaitable < void | Partial < Ctx > > ,
381+ ) : ContextMiddleware < Ctx , Partial < Ctx > > => ( {
382+ action : async ( ctx ) => ( ctx . request . skipNavigation ? { } : ( ( await action ( ctx ) ) ?? { } ) ) ,
383+ } ) ;
384+
361385 super ( {
362386 ...basicCrawlerOptions ,
363- contextPipelineBuilder : ( ) =>
364- contextPipelineBuilder ( )
365- . compose ( { action : this . performNavigation . bind ( this ) } )
387+ contextPipelineBuilder : ( ) => {
388+ let pipeline = contextPipelineBuilder ( ) . compose ( { action : this . prepareNavigation . bind ( this ) } ) ;
389+
390+ for ( const hook of this . preNavigationHooks ) {
391+ pipeline = pipeline . compose ( skipGuard ( hook ) ) ;
392+ }
393+
394+ pipeline = pipeline . compose ( skipGuard ( this . navigate . bind ( this ) ) ) ;
395+
396+ for ( const hook of this . postNavigationHooks ) {
397+ pipeline = pipeline . compose ( skipGuard ( hook ) ) ;
398+ }
399+
400+ return pipeline
401+ . compose ( skipGuard ( this . finalizeNavigation . bind ( this ) ) )
366402 . compose ( { action : this . handleBlockedRequestByContent . bind ( this ) } )
367- . compose ( { action : this . restoreRequestState . bind ( this ) } ) ,
403+ . compose ( { action : this . restoreRequestState . bind ( this ) } ) ;
404+ } ,
368405 extendContext : extendContext as ( context : Context ) => Awaitable < ContextExtension > ,
369406 } ) ;
370407
@@ -490,6 +527,9 @@ export abstract class BrowserCrawler<
490527 "The `response` property is not available. This might mean that you're trying to access it before navigation or that navigation resulted in `null` (this should only happen with `about:` URLs)" ,
491528 ) ;
492529 } ,
530+ get gotoOptions ( ) : Dictionary {
531+ throw new Error ( 'The `gotoOptions` property is not available until `prepareNavigation` runs.' ) ;
532+ } ,
493533 browserController : browserControllerInstance ,
494534 enqueueLinks : async ( enqueueOptions : EnqueueLinksOptions = { } ) => {
495535 return ( await browserCrawlerEnqueueLinks ( {
@@ -506,10 +546,7 @@ export abstract class BrowserCrawler<
506546 } ;
507547 }
508548
509- private async performNavigation ( crawlingContext : Context ) : Promise < {
510- request : LoadedRequest < Request > ;
511- response ?: Response ;
512- } > {
549+ private async prepareNavigation ( crawlingContext : Context ) : Promise < Partial < Context > > {
513550 if ( crawlingContext . request . skipNavigation ) {
514551 return {
515552 request : new Proxy ( crawlingContext . request , {
@@ -527,42 +564,56 @@ export abstract class BrowserCrawler<
527564 'The `response` property is not available - `skipNavigation` was used' ,
528565 ) ;
529566 } ,
530- } ;
567+ } as Partial < Context > ;
531568 }
532569
533- const gotoOptions = { timeout : this . navigationTimeoutMillis } as unknown as GoToOptions ;
570+ crawlingContext . request . state = RequestState . BEFORE_NAV ;
534571
535- const preNavigationHooksCookies = this . _getCookieHeaderFromRequest ( crawlingContext . request ) ;
572+ return {
573+ gotoOptions : { timeout : this . navigationTimeoutMillis } as unknown as GoToOptions ,
574+ [ COOKIES_BEFORE_HOOKS ] : this . _getCookieHeaderFromRequest ( crawlingContext . request ) ,
575+ } as unknown as Partial < Context > ;
576+ }
536577
537- crawlingContext . request . state = RequestState . BEFORE_NAV ;
538- await this . _executeHooks ( this . preNavigationHooks , crawlingContext , gotoOptions ) ;
578+ private async navigate ( crawlingContext : Context ) : Promise < Partial < Context > > {
539579 tryCancel ( ) ;
540580
541- const postNavigationHooksCookies = this . _getCookieHeaderFromRequest ( crawlingContext . request ) ;
581+ const gotoOptions = crawlingContext . gotoOptions as GoToOptions ;
582+ const cookiesBeforeHooks = readContextField < string > ( crawlingContext , COOKIES_BEFORE_HOOKS ) ;
583+ const cookiesAfterHooks = this . _getCookieHeaderFromRequest ( crawlingContext . request ) ;
542584
543- await this . _applyCookies ( crawlingContext , preNavigationHooksCookies , postNavigationHooksCookies ) ;
585+ await this . _applyCookies ( crawlingContext , cookiesBeforeHooks , cookiesAfterHooks ) ;
544586
545587 let response : Response | undefined ;
546-
547588 try {
548589 response = ( await this . _navigationHandler ( crawlingContext , gotoOptions ) ) ?? undefined ;
549590 } catch ( error ) {
550591 await this . _handleNavigationTimeout ( crawlingContext , error as Error ) ;
551-
552592 crawlingContext . request . state = RequestState . ERROR ;
553-
554593 this . _throwIfProxyError ( error as Error ) ;
555594 throw error ;
556595 }
557596 tryCancel ( ) ;
558597
559598 crawlingContext . request . state = RequestState . AFTER_NAV ;
560- await this . _executeHooks ( this . postNavigationHooks , crawlingContext , gotoOptions ) ;
599+
600+ return { response } as Partial < Context > ;
601+ }
602+
603+ private async finalizeNavigation ( crawlingContext : Context ) : Promise < Partial < Context > > {
604+ tryCancel ( ) ;
605+
606+ let response : Response | undefined ;
607+ try {
608+ response = crawlingContext . response ;
609+ } catch {
610+ // `preparePage` installs a throwing getter for `response`; reaching this branch means
611+ // navigation produced no response and no hook overrode it. Treat as undefined.
612+ }
561613
562614 await this . processResponse ( response , crawlingContext ) ;
563615 tryCancel ( ) ;
564616
565- // save cookies
566617 // TODO: Should we save the cookies also after/only the handle page?
567618 if ( this . saveResponseCookies && crawlingContext . session ) {
568619 const cookies = await crawlingContext . browserController . getCookies ( crawlingContext . page ) ;
@@ -579,16 +630,7 @@ export abstract class BrowserCrawler<
579630 }
580631 }
581632
582- if ( response !== undefined ) {
583- return {
584- request : crawlingContext . request as LoadedRequest < Request > ,
585- response,
586- } ;
587- }
588-
589- return {
590- request : crawlingContext . request as LoadedRequest < Request > ,
591- } ;
633+ return { request : crawlingContext . request as LoadedRequest < Request > } as Partial < Context > ;
592634 }
593635
594636 private async handleBlockedRequestByContent (
0 commit comments