@crawlee/browser 4.0.0-beta.95 → 4.0.0-beta.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -158,7 +158,11 @@ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Res
|
|
|
158
158
|
*/
|
|
159
159
|
postNavigationHooks?: BrowserHook<Context, ContextExtension>[];
|
|
160
160
|
/**
|
|
161
|
-
* Timeout
|
|
161
|
+
* Timeout for the whole navigation phase, in seconds. A single window shared by the `preNavigationHooks`,
|
|
162
|
+
* the page navigation, and the `postNavigationHooks` - so a slow hook eats into the same budget the
|
|
163
|
+
* navigation uses. Separate from the
|
|
164
|
+
* {@link BasicCrawlerOptions.requestHandlerTimeoutSecs|`requestHandlerTimeoutSecs`}, which times only the
|
|
165
|
+
* request handler.
|
|
162
166
|
*/
|
|
163
167
|
navigationTimeoutSecs?: number;
|
|
164
168
|
/**
|
|
@@ -332,6 +336,7 @@ export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPag
|
|
|
332
336
|
protected constructor(options: BrowserCrawlerOptions<Page, Response, Context, ContextExtension, ExtendedContext> & {
|
|
333
337
|
contextPipelineBuilder: () => ContextPipeline<CrawlingContext, Context>;
|
|
334
338
|
});
|
|
339
|
+
protected getNavigationTimeoutMillis(): number;
|
|
335
340
|
protected buildContextPipeline(): ContextPipeline<CrawlingContext, BrowserCrawlingContext<Page, Response, Dictionary>>;
|
|
336
341
|
private containsSelectors;
|
|
337
342
|
private isRequestBlocked;
|
|
@@ -339,6 +344,15 @@ export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPag
|
|
|
339
344
|
private prepareNavigation;
|
|
340
345
|
private navigate;
|
|
341
346
|
private finalizeNavigation;
|
|
347
|
+
/**
|
|
348
|
+
* Copies cookies from the live browser page into the session cookie jar.
|
|
349
|
+
*/
|
|
350
|
+
private persistCookiesFromPage;
|
|
351
|
+
/**
|
|
352
|
+
* Runs the user request handler, then re-reads browser cookies so login flows /
|
|
353
|
+
* `page.setCookie` / XHR `Set-Cookie` updates are stored for later requests.
|
|
354
|
+
*/
|
|
355
|
+
protected runRequestHandler(crawlingContext: ExtendedContext): Promise<void>;
|
|
342
356
|
private handleBlockedRequestByContent;
|
|
343
357
|
private restoreRequestState;
|
|
344
358
|
private applyCookies;
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { BasicCrawler, browserPoolCookieToToughCookie, ContextPipeline, cookieStringToToughCookie, enqueueLinks,
|
|
1
|
+
import { BasicCrawler, browserPoolCookieToToughCookie, ContextPipeline, cookieStringToToughCookie, enqueueLinks, NavigationSkippedError, OwnedOrInjected, remainingNavigationWindowMillis, RequestState, resolveBaseUrlForEnqueueLinksFiltering, SessionError, toughCookieToBrowserPoolCookie, tryAbsoluteURL, validators, } from '@crawlee/basic';
|
|
2
2
|
import { BrowserPool, RemoteBrowserPool } from '@crawlee/browser-pool';
|
|
3
3
|
import { CLOUDFLARE_RETRY_CSS_SELECTORS, RETRY_CSS_SELECTORS, sleep } from '@crawlee/utils';
|
|
4
4
|
import ow from 'ow';
|
|
5
|
-
import { tryCancel } from '@apify/timeout';
|
|
5
|
+
import { addTimeoutToPromise, TimeoutError, tryCancel } from '@apify/timeout';
|
|
6
6
|
const COOKIES_BEFORE_HOOKS = Symbol('cookiesBeforeHooks');
|
|
7
7
|
const readContextField = (ctx, key) => ctx[key];
|
|
8
|
+
/**
|
|
9
|
+
* Whether an error thrown by `page.goto()` is a navigation timeout - either our own {@link TimeoutError}
|
|
10
|
+
* or the driver's, which Playwright/Puppeteer report with their own class and a `Timeout ... exceeded` message
|
|
11
|
+
* naming the raw millisecond value rather than the configured window.
|
|
12
|
+
*/
|
|
13
|
+
function isNavigationTimeoutError(error) {
|
|
14
|
+
return (error instanceof TimeoutError ||
|
|
15
|
+
error?.name === 'TimeoutError' ||
|
|
16
|
+
error?.constructor?.name === 'TimeoutError' ||
|
|
17
|
+
/timeout.*exceeded/i.test(error?.message ?? ''));
|
|
18
|
+
}
|
|
8
19
|
/**
|
|
9
20
|
* Provides a simple framework for parallel crawling of web pages
|
|
10
21
|
* using headless browsers with [Puppeteer](https://github.com/puppeteer/puppeteer)
|
|
@@ -87,13 +98,24 @@ export class BrowserCrawler extends BasicCrawler {
|
|
|
87
98
|
super({
|
|
88
99
|
...basicCrawlerOptions,
|
|
89
100
|
contextPipelineBuilder: () => {
|
|
101
|
+
// A single navigation window covers the pre-navigation hooks, the navigation, and the
|
|
102
|
+
// post-navigation hooks: the whole phase shares one `navigationTimeoutSecs` budget, so a slow
|
|
103
|
+
// hook eats into the same window the navigation uses. The navigation itself is bounded by
|
|
104
|
+
// capping its `gotoOptions.timeout` to the remaining budget.
|
|
105
|
+
const windowGuard = (step) => skipGuard(async (ctx) => {
|
|
106
|
+
const remaining = remainingNavigationWindowMillis(ctx, this.navigationTimeoutMillis);
|
|
107
|
+
if (remaining <= 0) {
|
|
108
|
+
throw new TimeoutError(`Navigation timed out after ${this.navigationTimeoutMillis / 1000} seconds.`);
|
|
109
|
+
}
|
|
110
|
+
return addTimeoutToPromise(async () => step(ctx), remaining, `Navigation timed out after ${this.navigationTimeoutMillis / 1000} seconds.`);
|
|
111
|
+
});
|
|
90
112
|
let pipeline = contextPipelineBuilder().compose({ action: this.prepareNavigation.bind(this) });
|
|
91
113
|
for (const hook of this.preNavigationHooks) {
|
|
92
|
-
pipeline = pipeline.compose(
|
|
114
|
+
pipeline = pipeline.compose(windowGuard(hook));
|
|
93
115
|
}
|
|
94
116
|
pipeline = pipeline.compose(skipGuard(this.navigate.bind(this)));
|
|
95
117
|
for (const hook of this.postNavigationHooks) {
|
|
96
|
-
pipeline = pipeline.compose(
|
|
118
|
+
pipeline = pipeline.compose(windowGuard(hook));
|
|
97
119
|
}
|
|
98
120
|
return pipeline
|
|
99
121
|
.compose(skipGuard(this.finalizeNavigation.bind(this)))
|
|
@@ -145,6 +167,9 @@ export class BrowserCrawler extends BasicCrawler {
|
|
|
145
167
|
});
|
|
146
168
|
});
|
|
147
169
|
}
|
|
170
|
+
getNavigationTimeoutMillis() {
|
|
171
|
+
return this.navigationTimeoutMillis;
|
|
172
|
+
}
|
|
148
173
|
buildContextPipeline() {
|
|
149
174
|
return ContextPipeline.create().compose({
|
|
150
175
|
action: this.preparePage.bind(this),
|
|
@@ -236,6 +261,8 @@ export class BrowserCrawler extends BasicCrawler {
|
|
|
236
261
|
}
|
|
237
262
|
crawlingContext.request.state = RequestState.BEFORE_NAV;
|
|
238
263
|
return {
|
|
264
|
+
// Default to the full navigation timeout so a pre-navigation hook can read it; `navigate` narrows it
|
|
265
|
+
// to the remaining shared window unless a hook overrode it (see there).
|
|
239
266
|
gotoOptions: { timeout: this.navigationTimeoutMillis },
|
|
240
267
|
[COOKIES_BEFORE_HOOKS]: this._getCookieHeaderFromRequest(crawlingContext.request),
|
|
241
268
|
};
|
|
@@ -243,6 +270,18 @@ export class BrowserCrawler extends BasicCrawler {
|
|
|
243
270
|
async navigate(crawlingContext) {
|
|
244
271
|
tryCancel();
|
|
245
272
|
const gotoOptions = crawlingContext.gotoOptions;
|
|
273
|
+
const remaining = remainingNavigationWindowMillis(crawlingContext, this.navigationTimeoutMillis);
|
|
274
|
+
if (remaining <= 0) {
|
|
275
|
+
throw new TimeoutError(`Navigation timed out after ${this.navigationTimeoutMillis / 1000} seconds.`);
|
|
276
|
+
}
|
|
277
|
+
// If a hook left the default `navigationTimeoutMillis` in place, bound the goto to whatever is left of the
|
|
278
|
+
// shared navigation window. If it overrode the value - including `0`, Playwright's "no timeout" - honour
|
|
279
|
+
// that verbatim as the goto's own timeout. The driver enforces this natively (so a timed-out goto is
|
|
280
|
+
// aborted, not left lingering) and `handleNavigationTimeout` turns its error into our own message.
|
|
281
|
+
const gotoTimeout = gotoOptions;
|
|
282
|
+
if (gotoTimeout.timeout === this.navigationTimeoutMillis) {
|
|
283
|
+
gotoTimeout.timeout = remaining;
|
|
284
|
+
}
|
|
246
285
|
const cookiesBeforeHooks = readContextField(crawlingContext, COOKIES_BEFORE_HOOKS);
|
|
247
286
|
const cookiesAfterHooks = this._getCookieHeaderFromRequest(crawlingContext.request);
|
|
248
287
|
await this.applyCookies(crawlingContext, cookiesBeforeHooks, cookiesAfterHooks);
|
|
@@ -272,23 +311,51 @@ export class BrowserCrawler extends BasicCrawler {
|
|
|
272
311
|
}
|
|
273
312
|
await this.processResponse(response, crawlingContext);
|
|
274
313
|
tryCancel();
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
314
|
+
// Persist cookies from the navigation response before the user handler runs.
|
|
315
|
+
// Cookies set during `requestHandler` are saved again afterwards.
|
|
316
|
+
await this.persistCookiesFromPage(crawlingContext);
|
|
317
|
+
return { request: crawlingContext.request };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Copies cookies from the live browser page into the session cookie jar.
|
|
321
|
+
*/
|
|
322
|
+
async persistCookiesFromPage(crawlingContext) {
|
|
323
|
+
if (!this.saveResponseCookies || !crawlingContext.session) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const { cookies } = await this.browserPool.extractPageState(crawlingContext.page);
|
|
327
|
+
tryCancel();
|
|
328
|
+
// Prefer the live page URL — the handler may have navigated after the initial load.
|
|
329
|
+
const url = (await crawlingContext.page.url()) || crawlingContext.request.loadedUrl || crawlingContext.request.url;
|
|
330
|
+
for (const cookie of cookies) {
|
|
331
|
+
try {
|
|
332
|
+
crawlingContext.session.cookieJar.setCookieSync(browserPoolCookieToToughCookie(cookie), url, {
|
|
333
|
+
ignoreError: false,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (e) {
|
|
337
|
+
this.log.debug(`Could not set cookie: ${e.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Runs the user request handler, then re-reads browser cookies so login flows /
|
|
343
|
+
* `page.setCookie` / XHR `Set-Cookie` updates are stored for later requests.
|
|
344
|
+
*/
|
|
345
|
+
async runRequestHandler(crawlingContext) {
|
|
346
|
+
try {
|
|
347
|
+
await super.runRequestHandler(crawlingContext);
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
if (!crawlingContext.request.skipNavigation) {
|
|
281
351
|
try {
|
|
282
|
-
|
|
283
|
-
ignoreError: false,
|
|
284
|
-
});
|
|
352
|
+
await this.persistCookiesFromPage(crawlingContext);
|
|
285
353
|
}
|
|
286
|
-
catch
|
|
287
|
-
|
|
354
|
+
catch {
|
|
355
|
+
// Page may already be closed on some failure paths; ignore.
|
|
288
356
|
}
|
|
289
357
|
}
|
|
290
358
|
}
|
|
291
|
-
return { request: crawlingContext.request };
|
|
292
359
|
}
|
|
293
360
|
async handleBlockedRequestByContent(crawlingContext) {
|
|
294
361
|
if (this.retryOnBlocked) {
|
|
@@ -316,12 +383,15 @@ export class BrowserCrawler extends BasicCrawler {
|
|
|
316
383
|
*/
|
|
317
384
|
async handleNavigationTimeout(crawlingContext, error) {
|
|
318
385
|
const { session, page } = crawlingContext;
|
|
319
|
-
if (error?.constructor.name === 'TimeoutError') {
|
|
320
|
-
handleRequestTimeout({ session, errorMessage: error.message });
|
|
321
|
-
}
|
|
322
386
|
// Fire-and-forget: no user code will run on this page after a failed navigation.
|
|
323
387
|
// Swallow rejections: the page may already be detached.
|
|
324
388
|
void page.evaluate(() => window.stop()).catch(() => { });
|
|
389
|
+
if (isNavigationTimeoutError(error)) {
|
|
390
|
+
session?.markBad();
|
|
391
|
+
// The driver was handed the remaining window (usually shorter than `navigationTimeoutSecs` once the
|
|
392
|
+
// hooks have run), so it names that value in its own error; report the configured window instead.
|
|
393
|
+
throw new TimeoutError(`Navigation timed out after ${this.navigationTimeoutMillis / 1000} seconds.`);
|
|
394
|
+
}
|
|
325
395
|
}
|
|
326
396
|
/**
|
|
327
397
|
* Transforms proxy-related errors to `SessionError`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/browser",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.97",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@apify/timeout": "^0.
|
|
51
|
-
"@crawlee/basic": "4.0.0-beta.
|
|
52
|
-
"@crawlee/browser-pool": "4.0.0-beta.
|
|
53
|
-
"@crawlee/types": "4.0.0-beta.
|
|
54
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
50
|
+
"@apify/timeout": "^0.4.4",
|
|
51
|
+
"@crawlee/basic": "4.0.0-beta.97",
|
|
52
|
+
"@crawlee/browser-pool": "4.0.0-beta.97",
|
|
53
|
+
"@crawlee/types": "4.0.0-beta.97",
|
|
54
|
+
"@crawlee/utils": "4.0.0-beta.97",
|
|
55
55
|
"ow": "^2.0.0",
|
|
56
56
|
"tslib": "^2.8.1",
|
|
57
57
|
"type-fest": "^4.41.0"
|
|
@@ -75,5 +75,5 @@
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
|
-
"gitHead": "
|
|
78
|
+
"gitHead": "43008dd43f4832d083353ba1b731c0b4607c9cfa"
|
|
79
79
|
}
|