@crawlee/basic 4.0.0-beta.211 → 4.0.0-beta.212
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.
- package/internals/autoscaling/autoscaled_pool.d.ts +0 -20
- package/internals/autoscaling/autoscaled_pool.js +11 -12
- package/internals/autoscaling/concurrency_system.d.ts +0 -38
- package/internals/autoscaling/concurrency_system.js +51 -53
- package/internals/autoscaling/memory_load_signal.d.ts +0 -2
- package/internals/autoscaling/memory_load_signal.js +4 -4
- package/internals/autoscaling/system_status.d.ts +0 -10
- package/internals/autoscaling/system_status.js +5 -5
- package/internals/basic-crawler.d.ts +0 -88
- package/internals/basic-crawler.js +70 -67
- package/internals/crawlers/context_pipeline.js +3 -3
- package/internals/crawlers/error_tracker.d.ts +0 -1
- package/internals/crawlers/error_tracker.js +3 -3
- package/internals/crawlers/statistics.d.ts +0 -2
- package/internals/crawlers/statistics.js +11 -11
- package/internals/crawling_request.d.ts +1 -1
- package/internals/crawling_request.js +12 -12
- package/internals/router.d.ts +0 -9
- package/internals/router.js +5 -5
- package/internals/session_pool/session.d.ts +0 -5
- package/internals/session_pool/session.js +3 -3
- package/internals/session_pool/session_pool.d.ts +0 -51
- package/internals/session_pool/session_pool.js +36 -36
- package/internals/sitemap_request_loader.d.ts +0 -39
- package/internals/sitemap_request_loader.js +18 -18
- package/internals/throttling_request_manager.d.ts +0 -1
- package/internals/throttling_request_manager.js +5 -5
- package/package.json +7 -7
|
@@ -441,7 +441,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
441
441
|
get log(): CrawleeLogger;
|
|
442
442
|
protected readonly requestHandler: RequestHandler<ExtendedContext>;
|
|
443
443
|
protected readonly internalTimeoutMillis: number;
|
|
444
|
-
private get handledRequestsCount();
|
|
445
444
|
protected blockedStatusCodes: Set<number>;
|
|
446
445
|
protected readonly additionalHttpErrorStatusCodes: Set<number>;
|
|
447
446
|
protected readonly httpClient: BaseHttpClient;
|
|
@@ -514,22 +513,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
514
513
|
* @returns `true` if the status code is considered an error, `false` otherwise
|
|
515
514
|
*/
|
|
516
515
|
protected isErrorStatusCode(status: number): boolean;
|
|
517
|
-
/**
|
|
518
|
-
* Builds the basic context pipeline that transforms `{ request }` into a full `CrawlingContext`.
|
|
519
|
-
* This handles base context creation, session resolution, and context helpers.
|
|
520
|
-
*/
|
|
521
|
-
private buildBasicContextPipeline;
|
|
522
|
-
private checkRobotsTxt;
|
|
523
|
-
private createBaseContext;
|
|
524
|
-
/**
|
|
525
|
-
* Fetches the next request and rebuilds it as a {@link CrawlingRequest}. A copy rather than the manager's
|
|
526
|
-
* instance: managers key on `uniqueKey`/`id`, and it is this copy that flows back into `reclaimRequest` /
|
|
527
|
-
* `markRequestAsHandled`, so nothing observes the difference.
|
|
528
|
-
*/
|
|
529
|
-
private resolveRequest;
|
|
530
|
-
private resolveSession;
|
|
531
|
-
private createContextHelpers;
|
|
532
|
-
private buildFinalContextPipeline;
|
|
533
516
|
/**
|
|
534
517
|
* Checks if the given error is a proxy error by comparing its message to a list of known proxy error messages.
|
|
535
518
|
* Used for retrying requests that failed due to proxy errors.
|
|
@@ -547,7 +530,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
547
530
|
* their status-reporting backend (e.g. the Apify platform).
|
|
548
531
|
*/
|
|
549
532
|
setStatusMessage(message: string, options?: SetStatusMessageOptions): void;
|
|
550
|
-
private getPeriodicLogger;
|
|
551
533
|
/**
|
|
552
534
|
* Runs the crawler. Returns a promise that resolves once every request has been processed and the crawler's
|
|
553
535
|
* finished-check ({@link BasicCrawlerOptions.taskLoopOptions|`taskLoopOptions.isFinishedFunction`}, or the
|
|
@@ -596,26 +578,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
596
578
|
* crawler's request manager, which is no longer guaranteed to be a {@link RequestQueue}.
|
|
597
579
|
*/
|
|
598
580
|
getRequestQueue(): Promise<IRequestManager>;
|
|
599
|
-
/**
|
|
600
|
-
* Opens the default {@link RequestQueue} — the crawler's own, read from when the caller supplied nothing.
|
|
601
|
-
*/
|
|
602
|
-
private openOwnedRequestQueue;
|
|
603
|
-
/**
|
|
604
|
-
* Tells a request manager how long we expect to hold a fetched request, so that one backed by a
|
|
605
|
-
* locking storage backend keeps it reserved for slightly longer than the request handler timeout
|
|
606
|
-
* (with some padding for overhead), but never for less than a minute. This prevents a long-running
|
|
607
|
-
* request from being handed out a second time while it is still being processed — and it works
|
|
608
|
-
* regardless of whether the manager is a plain {@link RequestQueue} or a `RequestManagerTandem`.
|
|
609
|
-
*/
|
|
610
|
-
private applyRequestManagerTimeouts;
|
|
611
|
-
/**
|
|
612
|
-
* Validates a request source's `userData` against the {@link RouteSchemas|Standard Schema} registered
|
|
613
|
-
* for its label on the crawler's schema-router (if any), throwing a {@link RequestValidationError} on
|
|
614
|
-
* mismatch. A no-op when the user's request handler is not a schema-router, or no schema is registered for
|
|
615
|
-
* the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
|
|
616
|
-
* `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
|
|
617
|
-
*/
|
|
618
|
-
private validateRequestUserData;
|
|
619
581
|
useState<State extends Dictionary = Dictionary>(defaultValue?: State): Promise<State>;
|
|
620
582
|
/**
|
|
621
583
|
* Adds requests to the queue in batches. By default, it will resolve after the initial batch is added, and continue
|
|
@@ -661,26 +623,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
661
623
|
* browser crawlers override it with their `navigationTimeoutSecs`.
|
|
662
624
|
*/
|
|
663
625
|
protected getNavigationTimeoutMillis(): number;
|
|
664
|
-
/**
|
|
665
|
-
* Races the request against the internal timeout (see {@link raceWithTimeout}), sized to outlast the phases
|
|
666
|
-
* that have their own timeout - the navigation, its hooks, and the request handler - so a legitimately slow
|
|
667
|
-
* request, a per-route override, or a low `CRAWLEE_INTERNAL_TIMEOUT` is not cut short mid-phase. It takes
|
|
668
|
-
* whichever is larger: the configured internal timeout, or this request's combined phase budget.
|
|
669
|
-
*/
|
|
670
|
-
private withRequestTimeout;
|
|
671
|
-
/**
|
|
672
|
-
* The request handler timeout for a request with the given route label. A router route may override the
|
|
673
|
-
* crawler's own `requestHandlerTimeoutSecs`; anything else falls back to `fallbackMillis`.
|
|
674
|
-
*
|
|
675
|
-
* @param label The request's route label, or `undefined` for the default route / no specific request.
|
|
676
|
-
* @param fallbackMillis Timeout to use when no route overrides it.
|
|
677
|
-
*/
|
|
678
|
-
private resolveRequestHandlerTimeoutMillis;
|
|
679
|
-
/**
|
|
680
|
-
* The timeout the router route with the given label asked for, or `undefined` when it did not override one
|
|
681
|
-
* (or the request handler is not a router at all).
|
|
682
|
-
*/
|
|
683
|
-
private getRouteTimeoutMillis;
|
|
684
626
|
protected runRequestHandler(crawlingContext: ExtendedContext): Promise<void>;
|
|
685
627
|
/**
|
|
686
628
|
* Runs `callback` inside a {@link StorageTransaction}, unless transactional storage is disabled.
|
|
@@ -692,7 +634,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
692
634
|
* Handles blocked request
|
|
693
635
|
*/
|
|
694
636
|
protected throwOnBlockedRequest(statusCode: number): void;
|
|
695
|
-
private isAllowedBasedOnRobotsTxtFile;
|
|
696
637
|
/**
|
|
697
638
|
* Records an HTTP 429 against the URL's domain so the request manager can hold the retry back.
|
|
698
639
|
*
|
|
@@ -701,15 +642,8 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
701
642
|
* {@link RequestThrottledError} rather than treating the response as a blocked session.
|
|
702
643
|
*/
|
|
703
644
|
protected recordDomainRateLimit(url: string, retryAfterHeader?: string | null): boolean;
|
|
704
|
-
/** Hands a robots.txt `Crawl-delay` to the request manager, warning if it will not be honoured. */
|
|
705
|
-
private applyCrawlDelay;
|
|
706
645
|
/** @internal */
|
|
707
646
|
protected getRobotsTxtFileForUrl(url: string): Promise<RobotsTxtFile | undefined>;
|
|
708
|
-
private pauseOnMigration;
|
|
709
|
-
/**
|
|
710
|
-
* Fetches the next request to process from the underlying request provider.
|
|
711
|
-
*/
|
|
712
|
-
private fetchNextRequest;
|
|
713
647
|
/** Runs the request handler and marks the request as handled. Failures are left to `handleRequestFailure`. */
|
|
714
648
|
private handleRequest;
|
|
715
649
|
/**
|
|
@@ -717,21 +651,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
717
651
|
* @internal
|
|
718
652
|
*/
|
|
719
653
|
protected addCrawlDepthRequestGenerator(requests: RequestsLike, newRequestDepth: number): AsyncGenerator<Source, void, undefined>;
|
|
720
|
-
/**
|
|
721
|
-
* Run async callback with given timeout and retry. Returns the result of the callback.
|
|
722
|
-
* @ignore
|
|
723
|
-
*/
|
|
724
|
-
private timeoutAndRetry;
|
|
725
|
-
/**
|
|
726
|
-
* Whether the request manager has a request ready for processing. A manager that is only `waiting` also gets a
|
|
727
|
-
* wake-up scheduled, so a paced crawl resumes on its clock rather than on the task loop's polling interval.
|
|
728
|
-
*/
|
|
729
|
-
private isTaskReadyFunction;
|
|
730
|
-
/**
|
|
731
|
-
* Unwraps errors thrown by the context pipeline to get the actual user error.
|
|
732
|
-
* RequestHandlerError, ContextPipelineInitializationError and ContextPipelineCleanupError wrap the actual error.
|
|
733
|
-
*/
|
|
734
|
-
private unwrapError;
|
|
735
654
|
/**
|
|
736
655
|
* Handles errors thrown by user provided requestHandler()
|
|
737
656
|
*
|
|
@@ -745,12 +664,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
745
664
|
* @returns The message to be logged
|
|
746
665
|
*/
|
|
747
666
|
protected getMessageFromError(error: Error, forceStack?: boolean): string;
|
|
748
|
-
/**
|
|
749
|
-
* Whether the session should be spared for this error - either because it was already retired, or because the
|
|
750
|
-
* failure says nothing about the session (a rate limit is a property of the domain).
|
|
751
|
-
*/
|
|
752
|
-
private errorAbsolvesSession;
|
|
753
|
-
private canRequestBeRetried;
|
|
754
667
|
/**
|
|
755
668
|
* Ends the run in progress immediately, without waiting for the requests already in flight.
|
|
756
669
|
*
|
|
@@ -769,7 +682,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
769
682
|
destroy(): Promise<void>;
|
|
770
683
|
[Symbol.asyncDispose](): Promise<void>;
|
|
771
684
|
protected getCookieHeaderFromRequest(request: Request): string;
|
|
772
|
-
private requestMatchesEnqueueStrategy;
|
|
773
685
|
}
|
|
774
686
|
export interface CrawlerAddRequestsOptions extends AddRequestsBatchedOptions, EnqueueUrlsOptions {
|
|
775
687
|
}
|
|
@@ -184,14 +184,14 @@ export class BasicCrawler {
|
|
|
184
184
|
* Context built with this pipeline can be passed into multiple crawler pipelines at once.
|
|
185
185
|
*/
|
|
186
186
|
get #basicPipeline() {
|
|
187
|
-
this.#basicContextPipeline ??= this
|
|
187
|
+
this.#basicContextPipeline ??= this.#buildBasicContextPipeline();
|
|
188
188
|
return this.#basicContextPipeline;
|
|
189
189
|
}
|
|
190
190
|
#contextPipeline;
|
|
191
191
|
/** @internal */
|
|
192
192
|
get contextPipeline() {
|
|
193
193
|
if (this.#contextPipeline === undefined) {
|
|
194
|
-
this.#contextPipeline = this
|
|
194
|
+
this.#contextPipeline = this.#buildFinalContextPipeline();
|
|
195
195
|
}
|
|
196
196
|
return this.#contextPipeline;
|
|
197
197
|
}
|
|
@@ -212,7 +212,7 @@ export class BasicCrawler {
|
|
|
212
212
|
#maxRequestRetries;
|
|
213
213
|
#maxCrawlDepth;
|
|
214
214
|
#maxRequestsPerCrawl;
|
|
215
|
-
get handledRequestsCount() {
|
|
215
|
+
get #handledRequestsCount() {
|
|
216
216
|
return this.statistics.state.requestsSucceeded + this.statistics.state.requestsFailed;
|
|
217
217
|
}
|
|
218
218
|
#statusMessageLoggingInterval;
|
|
@@ -370,12 +370,12 @@ export class BasicCrawler {
|
|
|
370
370
|
throttleBy: 'registrableDomain',
|
|
371
371
|
persistStateKey: `CRAWLEE_THROTTLED_DOMAINS_${this.#identity.id}`,
|
|
372
372
|
// A factory, because the default queue is only opened on first use.
|
|
373
|
-
inner: suppliedManager ?? (() => this
|
|
373
|
+
inner: suppliedManager ?? (() => this.#openOwnedRequestQueue()),
|
|
374
374
|
})
|
|
375
375
|
: suppliedManager;
|
|
376
376
|
if (requestList !== undefined) {
|
|
377
377
|
// The list is read first, while new requests still have somewhere writable to go.
|
|
378
|
-
this.#requestManager = new RequestManagerTandem(requestList, writableManager ?? (() => this
|
|
378
|
+
this.#requestManager = new RequestManagerTandem(requestList, writableManager ?? (() => this.#openOwnedRequestQueue()));
|
|
379
379
|
}
|
|
380
380
|
else if (writableManager !== undefined) {
|
|
381
381
|
// A RequestQueue is itself a request manager.
|
|
@@ -440,7 +440,7 @@ export class BasicCrawler {
|
|
|
440
440
|
}
|
|
441
441
|
this.internalTimeoutMillis = Math.min(this.internalTimeoutMillis, maxSignedInteger);
|
|
442
442
|
this.#maxRequestsPerCrawl = maxRequestsPerCrawl;
|
|
443
|
-
const isMaxPagesExceeded = () => this.#maxRequestsPerCrawl && this.#maxRequestsPerCrawl <= this
|
|
443
|
+
const isMaxPagesExceeded = () => this.#maxRequestsPerCrawl && this.#maxRequestsPerCrawl <= this.#handledRequestsCount;
|
|
444
444
|
// eslint-disable-next-line prefer-const
|
|
445
445
|
let { isFinishedFunction, isTaskReadyFunction } = taskLoopOptions;
|
|
446
446
|
// override even if `isFinishedFunction` provided by user - `keepAlive` has higher priority
|
|
@@ -452,7 +452,7 @@ export class BasicCrawler {
|
|
|
452
452
|
const source = this.requestManager;
|
|
453
453
|
if (!source)
|
|
454
454
|
throw new Error('Request provider is not initialized!');
|
|
455
|
-
const request = await this
|
|
455
|
+
const request = await this.#resolveRequest();
|
|
456
456
|
if (!request) {
|
|
457
457
|
return;
|
|
458
458
|
}
|
|
@@ -469,20 +469,20 @@ export class BasicCrawler {
|
|
|
469
469
|
// Navigation, the navigation hooks and the request handler are timed individually, but the
|
|
470
470
|
// phases between them are not, so a request could still get stuck indefinitely. This is the
|
|
471
471
|
// catch-all for that - see `raceWithTimeout` for why it is a bare timer, not a timeout frame.
|
|
472
|
-
await this
|
|
472
|
+
await this.#withRequestTimeout(crawlingContext, this.#basicPipeline.chain(this.contextPipeline).call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request), (error) => this.#handleRequestFailure(error, crawlingContext, source, request))));
|
|
473
473
|
}
|
|
474
474
|
catch (error) {
|
|
475
475
|
// The internal timeout above fired, or `handleRequestFailure` threw a `SessionError` - both
|
|
476
476
|
// take the normal error flow. Anything else is fatal.
|
|
477
477
|
if (error instanceof SessionError || crawlingContext[timeoutExpiredKey]?.()) {
|
|
478
|
-
const unwrappedError = this
|
|
478
|
+
const unwrappedError = this.#unwrapError(error);
|
|
479
479
|
await this.requestFunctionErrorHandler(unwrappedError, crawlingContext, request, this.requestManager);
|
|
480
|
-
if (!this
|
|
480
|
+
if (!this.#errorAbsolvesSession(unwrappedError)) {
|
|
481
481
|
crawlingContext.session?.markBad();
|
|
482
482
|
}
|
|
483
483
|
return;
|
|
484
484
|
}
|
|
485
|
-
throw this
|
|
485
|
+
throw this.#unwrapError(error);
|
|
486
486
|
}
|
|
487
487
|
},
|
|
488
488
|
isTaskReadyFunction: async () => {
|
|
@@ -496,13 +496,13 @@ export class BasicCrawler {
|
|
|
496
496
|
'Ongoing requests will be allowed to complete.');
|
|
497
497
|
return false;
|
|
498
498
|
}
|
|
499
|
-
return isTaskReadyFunction ? await isTaskReadyFunction() : await this
|
|
499
|
+
return isTaskReadyFunction ? await isTaskReadyFunction() : await this.#isTaskReadyFunction();
|
|
500
500
|
},
|
|
501
501
|
isFinishedFunction: async () => {
|
|
502
502
|
if (isMaxPagesExceeded()) {
|
|
503
503
|
this.log.info(`Earlier, the crawler reached the maxRequestsPerCrawl limit of ${this.#maxRequestsPerCrawl} requests ` +
|
|
504
504
|
'and all requests that were in progress at that time have now finished. ' +
|
|
505
|
-
`In total, the crawler processed ${this
|
|
505
|
+
`In total, the crawler processed ${this.#handledRequestsCount} requests and will shut down.`);
|
|
506
506
|
return true;
|
|
507
507
|
}
|
|
508
508
|
if (this.#run?.stopRequested) {
|
|
@@ -571,15 +571,15 @@ export class BasicCrawler {
|
|
|
571
571
|
* Builds the basic context pipeline that transforms `{ request }` into a full `CrawlingContext`.
|
|
572
572
|
* This handles base context creation, session resolution, and context helpers.
|
|
573
573
|
*/
|
|
574
|
-
buildBasicContextPipeline() {
|
|
574
|
+
#buildBasicContextPipeline() {
|
|
575
575
|
return ContextPipeline.create()
|
|
576
|
-
.compose(this
|
|
577
|
-
.compose(this
|
|
578
|
-
.compose(this
|
|
579
|
-
.compose(this
|
|
576
|
+
.compose(this.#checkRobotsTxt.bind(this))
|
|
577
|
+
.compose(this.#createBaseContext.bind(this))
|
|
578
|
+
.compose(this.#resolveSession.bind(this))
|
|
579
|
+
.compose(this.#createContextHelpers.bind(this));
|
|
580
580
|
}
|
|
581
|
-
async checkRobotsTxt({ request }) {
|
|
582
|
-
if (!(await this
|
|
581
|
+
async #checkRobotsTxt({ request }) {
|
|
582
|
+
if (!(await this.#isAllowedBasedOnRobotsTxtFile(request.url))) {
|
|
583
583
|
this.log.warning(`Skipping request ${request.url} (${request.id}) because it is disallowed based on robots.txt`);
|
|
584
584
|
request.state = RequestState.SKIPPED;
|
|
585
585
|
request.noRetry = true;
|
|
@@ -591,7 +591,7 @@ export class BasicCrawler {
|
|
|
591
591
|
}
|
|
592
592
|
return {};
|
|
593
593
|
}
|
|
594
|
-
createBaseContext(context, onCleanup) {
|
|
594
|
+
#createBaseContext(context, onCleanup) {
|
|
595
595
|
return {
|
|
596
596
|
id: cryptoRandomObjectId(10),
|
|
597
597
|
log: this.log,
|
|
@@ -640,8 +640,8 @@ export class BasicCrawler {
|
|
|
640
640
|
* instance: managers key on `uniqueKey`/`id`, and it is this copy that flows back into `reclaimRequest` /
|
|
641
641
|
* `markRequestAsHandled`, so nothing observes the difference.
|
|
642
642
|
*/
|
|
643
|
-
async resolveRequest() {
|
|
644
|
-
const fetched = await this
|
|
643
|
+
async #resolveRequest() {
|
|
644
|
+
const fetched = await this.#timeoutAndRetry(this.#fetchNextRequest.bind(this), this.internalTimeoutMillis, `Fetching next request timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
645
645
|
if (!fetched) {
|
|
646
646
|
return null;
|
|
647
647
|
}
|
|
@@ -650,8 +650,8 @@ export class BasicCrawler {
|
|
|
650
650
|
request.loadedUrl = undefined;
|
|
651
651
|
return request;
|
|
652
652
|
}
|
|
653
|
-
async resolveSession({ request }) {
|
|
654
|
-
const session = await this
|
|
653
|
+
async #resolveSession({ request }) {
|
|
654
|
+
const session = await this.#timeoutAndRetry(async () => {
|
|
655
655
|
const existingSession = await this.sessionPool.getSession(request.sessionId);
|
|
656
656
|
if (!existingSession) {
|
|
657
657
|
throw new MissingSessionError(request.sessionId);
|
|
@@ -660,7 +660,7 @@ export class BasicCrawler {
|
|
|
660
660
|
}, this.internalTimeoutMillis, `Fetching session timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
661
661
|
return { session, proxyInfo: session?.proxyInfo };
|
|
662
662
|
}
|
|
663
|
-
async createContextHelpers({ request, session }) {
|
|
663
|
+
async #createContextHelpers({ request, session }) {
|
|
664
664
|
const addRequests = async (requests, options = {}) => {
|
|
665
665
|
const newCrawlDepth = request.crawlDepth + 1;
|
|
666
666
|
const requestsGenerator = this.addCrawlDepthRequestGenerator(requests, newCrawlDepth);
|
|
@@ -669,7 +669,7 @@ export class BasicCrawler {
|
|
|
669
669
|
const sendRequest = createSendRequest(this.httpClient, request, session);
|
|
670
670
|
return { addRequests, sendRequest };
|
|
671
671
|
}
|
|
672
|
-
buildFinalContextPipeline() {
|
|
672
|
+
#buildFinalContextPipeline() {
|
|
673
673
|
const subclassPipeline = (this.#contextPipelineOptions.contextPipelineBuilder?.() ??
|
|
674
674
|
ContextPipeline.create());
|
|
675
675
|
// `extendContext` runs *before* the subclass navigation pipeline (which includes the
|
|
@@ -694,7 +694,7 @@ export class BasicCrawler {
|
|
|
694
694
|
}
|
|
695
695
|
contextPipeline = contextPipeline.compose(async (context) => {
|
|
696
696
|
const { request } = context;
|
|
697
|
-
if (request && !this
|
|
697
|
+
if (request && !this.#requestMatchesEnqueueStrategy(request)) {
|
|
698
698
|
// eslint-disable-next-line dot-notation
|
|
699
699
|
const message = `Skipping request ${request.id} (starting url: ${request.url} -> loaded url: ${request.loadedUrl}) because it does not match the enqueue strategy (${request['enqueueStrategy']}).`;
|
|
700
700
|
this.log.debug(message);
|
|
@@ -741,7 +741,7 @@ export class BasicCrawler {
|
|
|
741
741
|
level: options.level,
|
|
742
742
|
});
|
|
743
743
|
}
|
|
744
|
-
getPeriodicLogger() {
|
|
744
|
+
#getPeriodicLogger() {
|
|
745
745
|
let previousState = { ...this.statistics.state };
|
|
746
746
|
const getOperationMode = () => {
|
|
747
747
|
const { requestsFailed } = this.statistics.state;
|
|
@@ -846,15 +846,15 @@ export class BasicCrawler {
|
|
|
846
846
|
await this.#concurrencySystem.ifOwned((system) => system.stop());
|
|
847
847
|
throw error;
|
|
848
848
|
}
|
|
849
|
-
const periodicLogger = this
|
|
849
|
+
const periodicLogger = this.#getPeriodicLogger();
|
|
850
850
|
this.setStatusMessage('Starting the crawler.', { level: 'INFO' });
|
|
851
851
|
const sigintHandler = async () => {
|
|
852
852
|
this.log.warning('Pausing... Press CTRL+C again to force exit. To resume, do: CRAWLEE_PURGE_ON_START=0 npm start');
|
|
853
|
-
await this
|
|
853
|
+
await this.#pauseOnMigration();
|
|
854
854
|
await run.abort();
|
|
855
855
|
};
|
|
856
856
|
// Attach a listener to handle migration and aborting events gracefully.
|
|
857
|
-
const boundPauseOnMigration = this
|
|
857
|
+
const boundPauseOnMigration = this.#pauseOnMigration.bind(this);
|
|
858
858
|
process.once('SIGINT', sigintHandler);
|
|
859
859
|
const eventManager = serviceLocator.getEventManager();
|
|
860
860
|
eventManager.on(EventType.MIGRATING, boundPauseOnMigration);
|
|
@@ -970,14 +970,14 @@ export class BasicCrawler {
|
|
|
970
970
|
*/
|
|
971
971
|
async getRequestManager() {
|
|
972
972
|
if (!this.#requestManager) {
|
|
973
|
-
this.#requestManager = await this
|
|
973
|
+
this.#requestManager = await this.#openOwnedRequestQueue();
|
|
974
974
|
}
|
|
975
975
|
// Apply the processing-time hint here (an async lifecycle point) rather than in the constructor,
|
|
976
976
|
// now that `setExpectedRequestProcessingTimeSecs` is async. The hint is raise-only and idempotent,
|
|
977
977
|
// but guard so we do not re-issue it on every call.
|
|
978
978
|
if (!this.#requestManagerTimeoutsApplied) {
|
|
979
979
|
this.#requestManagerTimeoutsApplied = true;
|
|
980
|
-
await this
|
|
980
|
+
await this.#applyRequestManagerTimeouts(this.#requestManager);
|
|
981
981
|
}
|
|
982
982
|
return this.#requestManager;
|
|
983
983
|
}
|
|
@@ -991,7 +991,7 @@ export class BasicCrawler {
|
|
|
991
991
|
/**
|
|
992
992
|
* Opens the default {@link RequestQueue} — the crawler's own, read from when the caller supplied nothing.
|
|
993
993
|
*/
|
|
994
|
-
async openOwnedRequestQueue() {
|
|
994
|
+
async #openOwnedRequestQueue() {
|
|
995
995
|
// The first crawler instance uses the default queue (null identifier);
|
|
996
996
|
// subsequent instances get their own queue via a unique alias so they don't collide.
|
|
997
997
|
const identifier = this.#identity.instanceIndex === 0 ? null : { alias: `__default_${this.#identity.id}__` };
|
|
@@ -1004,7 +1004,7 @@ export class BasicCrawler {
|
|
|
1004
1004
|
* request from being handed out a second time while it is still being processed — and it works
|
|
1005
1005
|
* regardless of whether the manager is a plain {@link RequestQueue} or a `RequestManagerTandem`.
|
|
1006
1006
|
*/
|
|
1007
|
-
async applyRequestManagerTimeouts(requestManager) {
|
|
1007
|
+
async #applyRequestManagerTimeouts(requestManager) {
|
|
1008
1008
|
// A router route may hold a request for longer than the crawler's own timeout, and we cannot know
|
|
1009
1009
|
// which routes a run will hit, so reserve for the longest one any route asked for. The hint is
|
|
1010
1010
|
// raise-only, so erring high here is safe.
|
|
@@ -1019,7 +1019,7 @@ export class BasicCrawler {
|
|
|
1019
1019
|
* the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
|
|
1020
1020
|
* `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
|
|
1021
1021
|
*/
|
|
1022
|
-
async validateRequestUserData(source) {
|
|
1022
|
+
async #validateRequestUserData(source) {
|
|
1023
1023
|
if (typeof source === 'string') {
|
|
1024
1024
|
return;
|
|
1025
1025
|
}
|
|
@@ -1062,7 +1062,9 @@ export class BasicCrawler {
|
|
|
1062
1062
|
if (this.#maxRequestsPerCrawl === undefined) {
|
|
1063
1063
|
return explicitLimit;
|
|
1064
1064
|
}
|
|
1065
|
-
const limit = Math.max(0, this.#maxRequestsPerCrawl -
|
|
1065
|
+
const limit = Math.max(0, this.#maxRequestsPerCrawl -
|
|
1066
|
+
this.#handledRequestsCount -
|
|
1067
|
+
(await this.#getPendingRequestCountApproximation()));
|
|
1066
1068
|
return Math.min(limit, explicitLimit ?? Infinity);
|
|
1067
1069
|
}
|
|
1068
1070
|
async #handleSkippedRequest(options) {
|
|
@@ -1109,7 +1111,7 @@ export class BasicCrawler {
|
|
|
1109
1111
|
// its own per-item validation below, and validating an absent label/userData here would spuriously
|
|
1110
1112
|
// check them against a registered default-route schema.
|
|
1111
1113
|
if (options.label !== undefined || options.userData !== undefined) {
|
|
1112
|
-
await this
|
|
1114
|
+
await this.#validateRequestUserData({ label: options.label, userData: options.userData });
|
|
1113
1115
|
}
|
|
1114
1116
|
const requestLimit = await this.#calculateEnqueuedRequestLimit(options.limit);
|
|
1115
1117
|
const strategy = options.strategy ?? EnqueueStrategy.All;
|
|
@@ -1118,9 +1120,9 @@ export class BasicCrawler {
|
|
|
1118
1120
|
// The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
|
|
1119
1121
|
// (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
|
|
1120
1122
|
const enqueueStrategyPatterns = options.baseUrl ? buildEnqueueStrategyPatterns(options.baseUrl, strategy) : [];
|
|
1121
|
-
const isAllowedBasedOnRobotsTxtFile = this
|
|
1123
|
+
const isAllowedBasedOnRobotsTxtFile = this.#isAllowedBasedOnRobotsTxtFile.bind(this);
|
|
1122
1124
|
const maxCrawlDepth = this.#maxCrawlDepth;
|
|
1123
|
-
const validateRequestUserData = this
|
|
1125
|
+
const validateRequestUserData = this.#validateRequestUserData.bind(this);
|
|
1124
1126
|
const allSkipped = [];
|
|
1125
1127
|
// A skipped source (which can carry arbitrary userData) is only retained if something reads it -
|
|
1126
1128
|
// otherwise the URL alone is enough to build the callback argument and to log with.
|
|
@@ -1292,7 +1294,7 @@ export class BasicCrawler {
|
|
|
1292
1294
|
// floored per request so it will not actually cut them short, but the configured value is then effectively
|
|
1293
1295
|
// ignored, which is worth flagging. Checked here (not in the constructor) because a subclass sets its
|
|
1294
1296
|
// navigation timeout only after `super()`.
|
|
1295
|
-
const phasesMillis = this.getNavigationTimeoutMillis() + this
|
|
1297
|
+
const phasesMillis = this.getNavigationTimeoutMillis() + this.#resolveRequestHandlerTimeoutMillis(undefined);
|
|
1296
1298
|
if (this.internalTimeoutMillis < phasesMillis) {
|
|
1297
1299
|
this.log.warning(`CRAWLEE_INTERNAL_TIMEOUT (${this.internalTimeoutMillis / 1000}s) is shorter than the navigation ` +
|
|
1298
1300
|
`and request handler timeouts combined (${phasesMillis / 1000}s); it will be raised per request ` +
|
|
@@ -1314,9 +1316,9 @@ export class BasicCrawler {
|
|
|
1314
1316
|
* request, a per-route override, or a low `CRAWLEE_INTERNAL_TIMEOUT` is not cut short mid-phase. It takes
|
|
1315
1317
|
* whichever is larger: the configured internal timeout, or this request's combined phase budget.
|
|
1316
1318
|
*/
|
|
1317
|
-
async withRequestTimeout(crawlingContext, work) {
|
|
1319
|
+
async #withRequestTimeout(crawlingContext, work) {
|
|
1318
1320
|
const { request } = crawlingContext;
|
|
1319
|
-
const phasesMillis = this.getNavigationTimeoutMillis() + this
|
|
1321
|
+
const phasesMillis = this.getNavigationTimeoutMillis() + this.#resolveRequestHandlerTimeoutMillis(request.label);
|
|
1320
1322
|
const timeoutMillis = Math.max(this.internalTimeoutMillis, phasesMillis);
|
|
1321
1323
|
await raceWithTimeout(crawlingContext, work, { timeoutMillis, requestId: request.id });
|
|
1322
1324
|
}
|
|
@@ -1327,14 +1329,14 @@ export class BasicCrawler {
|
|
|
1327
1329
|
* @param label The request's route label, or `undefined` for the default route / no specific request.
|
|
1328
1330
|
* @param fallbackMillis Timeout to use when no route overrides it.
|
|
1329
1331
|
*/
|
|
1330
|
-
resolveRequestHandlerTimeoutMillis(label, fallbackMillis = this.#requestHandlerTimeoutMillis) {
|
|
1331
|
-
return this
|
|
1332
|
+
#resolveRequestHandlerTimeoutMillis(label, fallbackMillis = this.#requestHandlerTimeoutMillis) {
|
|
1333
|
+
return this.#getRouteTimeoutMillis(label) ?? fallbackMillis;
|
|
1332
1334
|
}
|
|
1333
1335
|
/**
|
|
1334
1336
|
* The timeout the router route with the given label asked for, or `undefined` when it did not override one
|
|
1335
1337
|
* (or the request handler is not a router at all).
|
|
1336
1338
|
*/
|
|
1337
|
-
getRouteTimeoutMillis(label) {
|
|
1339
|
+
#getRouteTimeoutMillis(label) {
|
|
1338
1340
|
const getTimeoutSecs = this.requestHandler.getTimeoutSecs;
|
|
1339
1341
|
if (typeof getTimeoutSecs !== 'function') {
|
|
1340
1342
|
return undefined;
|
|
@@ -1343,7 +1345,7 @@ export class BasicCrawler {
|
|
|
1343
1345
|
return timeoutSecs === undefined ? undefined : timeoutSecs * 1000;
|
|
1344
1346
|
}
|
|
1345
1347
|
async runRequestHandler(crawlingContext) {
|
|
1346
|
-
const timeoutMillis = this
|
|
1348
|
+
const timeoutMillis = this.#resolveRequestHandlerTimeoutMillis(crawlingContext.request.label);
|
|
1347
1349
|
await addTimeoutToPromise(async () => this.requestHandler(crawlingContext), timeoutMillis, `requestHandler timed out after ${timeoutMillis / 1000} seconds (${crawlingContext.request.id}).`);
|
|
1348
1350
|
}
|
|
1349
1351
|
/**
|
|
@@ -1351,6 +1353,7 @@ export class BasicCrawler {
|
|
|
1351
1353
|
* Deliberately does **not** commit on return - the pipeline hands failures to `handleRequestFailure`, so a
|
|
1352
1354
|
* normal return says nothing about success. `handleRequest` commits on success.
|
|
1353
1355
|
*/
|
|
1356
|
+
// oxlint-disable-next-line crawlee/prefer-private-fields -- accessed by tests
|
|
1354
1357
|
async runInStorageTransaction(callback) {
|
|
1355
1358
|
if (!this.#transactionalStorageEnabled) {
|
|
1356
1359
|
return callback();
|
|
@@ -1390,7 +1393,7 @@ export class BasicCrawler {
|
|
|
1390
1393
|
throw new SessionError(`Request blocked - received ${statusCode} status code.`);
|
|
1391
1394
|
}
|
|
1392
1395
|
}
|
|
1393
|
-
async isAllowedBasedOnRobotsTxtFile(url) {
|
|
1396
|
+
async #isAllowedBasedOnRobotsTxtFile(url) {
|
|
1394
1397
|
if (!this.#respectRobotsTxtFile) {
|
|
1395
1398
|
return true;
|
|
1396
1399
|
}
|
|
@@ -1399,7 +1402,7 @@ export class BasicCrawler {
|
|
|
1399
1402
|
if (robotsTxtFile) {
|
|
1400
1403
|
const crawlDelay = robotsTxtFile.getCrawlDelay(userAgent);
|
|
1401
1404
|
if (crawlDelay !== undefined) {
|
|
1402
|
-
this
|
|
1405
|
+
this.#applyCrawlDelay(url, crawlDelay);
|
|
1403
1406
|
}
|
|
1404
1407
|
}
|
|
1405
1408
|
return !robotsTxtFile || robotsTxtFile.isAllowed(url, userAgent);
|
|
@@ -1427,7 +1430,7 @@ export class BasicCrawler {
|
|
|
1427
1430
|
return false;
|
|
1428
1431
|
}
|
|
1429
1432
|
/** Hands a robots.txt `Crawl-delay` to the request manager, warning if it will not be honoured. */
|
|
1430
|
-
applyCrawlDelay(url, delaySeconds) {
|
|
1433
|
+
#applyCrawlDelay(url, delaySeconds) {
|
|
1431
1434
|
// robots.txt is per-origin; `hostname` is the closest pacing scope and errs wide (http and https to one
|
|
1432
1435
|
// host share a clock).
|
|
1433
1436
|
if (this.requestManager?.recordPacingSignal({
|
|
@@ -1464,7 +1467,7 @@ export class BasicCrawler {
|
|
|
1464
1467
|
return undefined;
|
|
1465
1468
|
}
|
|
1466
1469
|
}
|
|
1467
|
-
async pauseOnMigration() {
|
|
1470
|
+
async #pauseOnMigration() {
|
|
1468
1471
|
const run = this.#dispatchingRun;
|
|
1469
1472
|
if (run) {
|
|
1470
1473
|
await run.pause(SAFE_MIGRATION_WAIT_MILLIS).catch((err) => {
|
|
@@ -1484,7 +1487,7 @@ export class BasicCrawler {
|
|
|
1484
1487
|
/**
|
|
1485
1488
|
* Fetches the next request to process from the underlying request provider.
|
|
1486
1489
|
*/
|
|
1487
|
-
async fetchNextRequest() {
|
|
1490
|
+
async #fetchNextRequest() {
|
|
1488
1491
|
if (this.requestManager === undefined) {
|
|
1489
1492
|
throw new Error(`fetchNextRequest called on an uninitialized crawler`);
|
|
1490
1493
|
}
|
|
@@ -1505,7 +1508,7 @@ export class BasicCrawler {
|
|
|
1505
1508
|
// it is retried. This also closes the transaction, so everything below passes through.
|
|
1506
1509
|
// Opened by `runInStorageTransaction`; absent when disabled or when the subclass opens its own.
|
|
1507
1510
|
await currentStorageTransaction()?.commit();
|
|
1508
|
-
await this
|
|
1511
|
+
await this.#timeoutAndRetry(async () => requestSource.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
1509
1512
|
this.statistics.recordRequestSuccess(request.id || request.uniqueKey, request.retryCount);
|
|
1510
1513
|
// reclaim session if request finishes successfully
|
|
1511
1514
|
request.state = RequestState.DONE;
|
|
@@ -1522,7 +1525,7 @@ export class BasicCrawler {
|
|
|
1522
1525
|
currentStorageTransaction()?.rollback();
|
|
1523
1526
|
const skipped = rawError instanceof ContextPipelineInitializationError &&
|
|
1524
1527
|
rawError.cause instanceof ContextPipelineInterruptedError;
|
|
1525
|
-
const err = this
|
|
1528
|
+
const err = this.#unwrapError(rawError);
|
|
1526
1529
|
const context = crawlingContext;
|
|
1527
1530
|
const retryCountBefore = request.retryCount;
|
|
1528
1531
|
const resumeRequestTimeout = crawlingContext[suspendTimeoutKey]?.();
|
|
@@ -1531,7 +1534,7 @@ export class BasicCrawler {
|
|
|
1531
1534
|
// (e.g., doesn't match enqueue strategy after redirect). Just return gracefully.
|
|
1532
1535
|
if (skipped) {
|
|
1533
1536
|
this.statistics.discardRequestRecord(request.id || request.uniqueKey);
|
|
1534
|
-
await this
|
|
1537
|
+
await this.#timeoutAndRetry(async () => requestSource.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
1535
1538
|
return;
|
|
1536
1539
|
}
|
|
1537
1540
|
request.state = RequestState.ERROR_HANDLER;
|
|
@@ -1539,7 +1542,7 @@ export class BasicCrawler {
|
|
|
1539
1542
|
request.state = RequestState.DONE;
|
|
1540
1543
|
}
|
|
1541
1544
|
catch (secondaryError) {
|
|
1542
|
-
const unwrappedSecondaryError = this
|
|
1545
|
+
const unwrappedSecondaryError = this.#unwrapError(secondaryError);
|
|
1543
1546
|
const timedOut = unwrappedSecondaryError instanceof TimeoutError;
|
|
1544
1547
|
request.state = RequestState.ERROR;
|
|
1545
1548
|
if (timedOut) {
|
|
@@ -1575,7 +1578,7 @@ export class BasicCrawler {
|
|
|
1575
1578
|
}
|
|
1576
1579
|
// decrease the session score if the request fails (but the error handler did not throw);
|
|
1577
1580
|
// skip when the error is a SessionError, which already retired the session
|
|
1578
|
-
if (!skipped && !this
|
|
1581
|
+
if (!skipped && !this.#errorAbsolvesSession(err)) {
|
|
1579
1582
|
context.session?.markBad();
|
|
1580
1583
|
}
|
|
1581
1584
|
}
|
|
@@ -1603,7 +1606,7 @@ export class BasicCrawler {
|
|
|
1603
1606
|
* Run async callback with given timeout and retry. Returns the result of the callback.
|
|
1604
1607
|
* @ignore
|
|
1605
1608
|
*/
|
|
1606
|
-
async timeoutAndRetry(handler, timeout, error, maxRetries = 3, retried = 1) {
|
|
1609
|
+
async #timeoutAndRetry(handler, timeout, error, maxRetries = 3, retried = 1) {
|
|
1607
1610
|
try {
|
|
1608
1611
|
return await addTimeoutToPromise(handler, timeout, error);
|
|
1609
1612
|
}
|
|
@@ -1611,7 +1614,7 @@ export class BasicCrawler {
|
|
|
1611
1614
|
if (retried <= maxRetries) {
|
|
1612
1615
|
// we retry on any error, not just timeout
|
|
1613
1616
|
this.log.warning(`${e.message} (retrying ${retried}/${maxRetries})`);
|
|
1614
|
-
return this
|
|
1617
|
+
return this.#timeoutAndRetry(handler, timeout, error, maxRetries, retried + 1);
|
|
1615
1618
|
}
|
|
1616
1619
|
throw e;
|
|
1617
1620
|
}
|
|
@@ -1620,7 +1623,7 @@ export class BasicCrawler {
|
|
|
1620
1623
|
* Whether the request manager has a request ready for processing. A manager that is only `waiting` also gets a
|
|
1621
1624
|
* wake-up scheduled, so a paced crawl resumes on its clock rather than on the task loop's polling interval.
|
|
1622
1625
|
*/
|
|
1623
|
-
async isTaskReadyFunction() {
|
|
1626
|
+
async #isTaskReadyFunction() {
|
|
1624
1627
|
if (this.requestManager === undefined) {
|
|
1625
1628
|
return false;
|
|
1626
1629
|
}
|
|
@@ -1634,11 +1637,11 @@ export class BasicCrawler {
|
|
|
1634
1637
|
* Unwraps errors thrown by the context pipeline to get the actual user error.
|
|
1635
1638
|
* RequestHandlerError, ContextPipelineInitializationError and ContextPipelineCleanupError wrap the actual error.
|
|
1636
1639
|
*/
|
|
1637
|
-
unwrapError(error) {
|
|
1640
|
+
#unwrapError(error) {
|
|
1638
1641
|
if (error instanceof RequestHandlerError ||
|
|
1639
1642
|
error instanceof ContextPipelineInitializationError ||
|
|
1640
1643
|
error instanceof ContextPipelineCleanupError) {
|
|
1641
|
-
return this
|
|
1644
|
+
return this.#unwrapError(error.cause);
|
|
1642
1645
|
}
|
|
1643
1646
|
return error;
|
|
1644
1647
|
}
|
|
@@ -1663,7 +1666,7 @@ export class BasicCrawler {
|
|
|
1663
1666
|
if (error instanceof CriticalError) {
|
|
1664
1667
|
throw error;
|
|
1665
1668
|
}
|
|
1666
|
-
const shouldRetryRequest = this
|
|
1669
|
+
const shouldRetryRequest = this.#canRequestBeRetried(request, error);
|
|
1667
1670
|
if (shouldRetryRequest) {
|
|
1668
1671
|
await this.statistics.errorTrackerRetry.addAsync(error, crawlingContext);
|
|
1669
1672
|
await this.#errorHandler?.(crawlingContext, // valid cast - ExtendedContext transitively extends CrawlingContext
|
|
@@ -1741,10 +1744,10 @@ export class BasicCrawler {
|
|
|
1741
1744
|
* Whether the session should be spared for this error - either because it was already retired, or because the
|
|
1742
1745
|
* failure says nothing about the session (a rate limit is a property of the domain).
|
|
1743
1746
|
*/
|
|
1744
|
-
errorAbsolvesSession(error) {
|
|
1747
|
+
#errorAbsolvesSession(error) {
|
|
1745
1748
|
return error instanceof SessionError || error instanceof RequestThrottledError;
|
|
1746
1749
|
}
|
|
1747
|
-
canRequestBeRetried(request, error) {
|
|
1750
|
+
#canRequestBeRetried(request, error) {
|
|
1748
1751
|
// Request should never be retried, or the error encountered makes it not able to be retried.
|
|
1749
1752
|
if (request.noRetry || error instanceof NonRetryableError) {
|
|
1750
1753
|
return false;
|
|
@@ -1802,7 +1805,7 @@ export class BasicCrawler {
|
|
|
1802
1805
|
}
|
|
1803
1806
|
return request.headers?.Cookie || request.headers?.cookie || '';
|
|
1804
1807
|
}
|
|
1805
|
-
requestMatchesEnqueueStrategy(request) {
|
|
1808
|
+
#requestMatchesEnqueueStrategy(request) {
|
|
1806
1809
|
// If `skipNavigation` was used, just return `true`
|
|
1807
1810
|
try {
|
|
1808
1811
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|