@crawlee/basic 4.0.0-beta.80 → 4.0.0-beta.82
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/basic-crawler.d.ts +46 -47
- package/internals/basic-crawler.js +43 -43
- package/package.json +7 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, AutoscaledPoolOptions, Configuration, CrawleeLogger, CrawlingContext, DatasetExportOptions, EnqueueLinksOptions, EventManager, FinalStatistics, GetUserDataFromRequest, IRequestLoader, IRequestManager,
|
|
1
|
+
import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, AutoscaledPoolOptions, Configuration, CrawleeLogger, CrawlingContext, DatasetExportOptions, EnqueueLinksOptions, EventManager, FinalStatistics, GetUserDataFromRequest, IProxyConfiguration, IRequestLoader, IRequestManager, Request, RequestsLike, RouterHandler, RouterRoutes, SkippedRequestCallback, Source, StatisticsOptions, StatisticState, StorageIdentifier } from '@crawlee/core';
|
|
2
2
|
import { AutoscaledPool, ContextPipeline, Dataset, RequestQueue, Statistics } from '@crawlee/core';
|
|
3
3
|
import type { Awaitable, BaseHttpClient, BatchAddRequestsResult, Dictionary, ISession, ISessionPool, ProxyInfo, SetStatusMessageOptions, StorageBackend } from '@crawlee/types';
|
|
4
4
|
import { RobotsTxtFile } from '@crawlee/utils';
|
|
@@ -46,7 +46,13 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
46
46
|
*/
|
|
47
47
|
requestHandler?: RequestHandler<ExtendedContext>;
|
|
48
48
|
/**
|
|
49
|
-
* Allows the user to extend the crawling context
|
|
49
|
+
* Allows the user to extend the crawling context with custom functionality (helpers, references, etc.).
|
|
50
|
+
*
|
|
51
|
+
* `extendContext` runs *before* navigation, so the returned members are visible to the
|
|
52
|
+
* `preNavigationHooks`, `postNavigationHooks`, and the `requestHandler` alike. As a consequence,
|
|
53
|
+
* the `context` passed to `extendContext` is the pre-navigation {@link CrawlingContext} and does
|
|
54
|
+
* **not** include navigation-dependent members (e.g. `page`, `response`, `$`, `body`). If you need
|
|
55
|
+
* those, use a `postNavigationHook` or the `requestHandler` instead.
|
|
50
56
|
*
|
|
51
57
|
* **Example usage:**
|
|
52
58
|
*
|
|
@@ -66,7 +72,7 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
66
72
|
* });
|
|
67
73
|
* ```
|
|
68
74
|
*/
|
|
69
|
-
extendContext?: (context:
|
|
75
|
+
extendContext?: (context: CrawlingContext) => Awaitable<ContextExtension>;
|
|
70
76
|
/**
|
|
71
77
|
* *Intended for BasicCrawler subclasses*. Prepares a context pipeline that transforms the initial crawling context into the shape given by the `Context` type parameter.
|
|
72
78
|
*
|
|
@@ -252,7 +258,7 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
252
258
|
* If set, the crawler will be configured for all connections to use
|
|
253
259
|
* the Proxy URLs provided and rotated according to the configuration.
|
|
254
260
|
*/
|
|
255
|
-
proxyConfiguration?:
|
|
261
|
+
proxyConfiguration?: IProxyConfiguration;
|
|
256
262
|
/**
|
|
257
263
|
* Custom configuration to use for this crawler.
|
|
258
264
|
* If provided, the crawler will use its own ServiceLocator instance instead of the global one.
|
|
@@ -409,7 +415,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
409
415
|
* A reference to the underlying session pool that manages the crawler's {@link Session|sessions}. Typed as
|
|
410
416
|
* {@link ISessionPool} so custom implementations can be plugged in via the `sessionPool` constructor option.
|
|
411
417
|
*/
|
|
412
|
-
sessionPool: ISessionPool;
|
|
418
|
+
readonly sessionPool: ISessionPool;
|
|
413
419
|
/**
|
|
414
420
|
* Set when the crawler constructed its own {@link SessionPool} (no `sessionPool` option was provided).
|
|
415
421
|
* Holds the same instance as `sessionPool`, but typed as the concrete class so the crawler can call
|
|
@@ -438,10 +444,10 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
438
444
|
*/
|
|
439
445
|
autoscaledPool?: AutoscaledPool;
|
|
440
446
|
/**
|
|
441
|
-
* A reference to the underlying {@link
|
|
447
|
+
* A reference to the underlying {@link IProxyConfiguration} instance that manages the crawler's proxies.
|
|
442
448
|
* Only available if used by the crawler.
|
|
443
449
|
*/
|
|
444
|
-
proxyConfiguration?:
|
|
450
|
+
readonly proxyConfiguration?: IProxyConfiguration;
|
|
445
451
|
/**
|
|
446
452
|
* Default {@link Router} instance that will be used if we don't specify any {@link BasicCrawlerOptions.requestHandler|`requestHandler`}.
|
|
447
453
|
* See {@link Router.addHandler|`router.addHandler()`} and {@link Router.addDefaultHandler|`router.addDefaultHandler()`}.
|
|
@@ -463,33 +469,29 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
463
469
|
get contextPipeline(): ContextPipeline<CrawlingContext, ExtendedContext>;
|
|
464
470
|
running: boolean;
|
|
465
471
|
hasFinishedBefore: boolean;
|
|
466
|
-
|
|
472
|
+
private unexpectedStop;
|
|
467
473
|
get log(): CrawleeLogger;
|
|
468
|
-
protected requestHandler: RequestHandler<ExtendedContext>;
|
|
469
|
-
protected errorHandler?: ErrorHandler<CrawlingContext, ExtendedContext>;
|
|
470
|
-
protected failedRequestHandler?: ErrorHandler<CrawlingContext, ExtendedContext>;
|
|
471
|
-
|
|
472
|
-
protected internalTimeoutMillis: number;
|
|
473
|
-
protected maxRequestRetries: number;
|
|
474
|
-
protected maxCrawlDepth?: number;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
protected maxRequestsPerCrawl?: number;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
protected statusMessageLoggingInterval: number;
|
|
482
|
-
protected statusMessageCallback?: StatusMessageCallback;
|
|
474
|
+
protected readonly requestHandler: RequestHandler<ExtendedContext>;
|
|
475
|
+
protected readonly errorHandler?: ErrorHandler<CrawlingContext, ExtendedContext>;
|
|
476
|
+
protected readonly failedRequestHandler?: ErrorHandler<CrawlingContext, ExtendedContext>;
|
|
477
|
+
private requestHandlerTimeoutMillis;
|
|
478
|
+
protected readonly internalTimeoutMillis: number;
|
|
479
|
+
protected readonly maxRequestRetries: number;
|
|
480
|
+
protected readonly maxCrawlDepth?: number;
|
|
481
|
+
private sameDomainDelayMillis;
|
|
482
|
+
private domainAccessedTime;
|
|
483
|
+
protected readonly maxRequestsPerCrawl?: number;
|
|
484
|
+
private get handledRequestsCount();
|
|
485
|
+
private statusMessageLoggingInterval;
|
|
486
|
+
private statusMessageCallback?;
|
|
483
487
|
protected blockedStatusCodes: Set<number>;
|
|
484
|
-
protected additionalHttpErrorStatusCodes: Set<number>;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
protected httpClient: BaseHttpClient;
|
|
488
|
-
protected retryOnBlocked: boolean;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
};
|
|
492
|
-
protected onSkippedRequest?: SkippedRequestCallback;
|
|
488
|
+
protected readonly additionalHttpErrorStatusCodes: Set<number>;
|
|
489
|
+
private ignoreHttpErrorStatusCodes;
|
|
490
|
+
private autoscaledPoolOptions;
|
|
491
|
+
protected readonly httpClient: BaseHttpClient;
|
|
492
|
+
protected readonly retryOnBlocked: boolean;
|
|
493
|
+
private respectRobotsTxtFile;
|
|
494
|
+
protected readonly onSkippedRequest?: SkippedRequestCallback;
|
|
493
495
|
private _closeEvents?;
|
|
494
496
|
private loggedPerRun;
|
|
495
497
|
private readonly robotsTxtFileCache;
|
|
@@ -580,9 +582,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
580
582
|
* Builds the basic context pipeline that transforms `{ request }` into a full `CrawlingContext`.
|
|
581
583
|
* This handles base context creation, session resolution, and context helpers.
|
|
582
584
|
*/
|
|
583
|
-
|
|
584
|
-
request: Request;
|
|
585
|
-
}, CrawlingContext>;
|
|
585
|
+
private buildBasicContextPipeline;
|
|
586
586
|
private checkRobotsTxt;
|
|
587
587
|
/**
|
|
588
588
|
* Builds the subclass-specific context pipeline that transforms a `CrawlingContext` into the crawler's target context type.
|
|
@@ -662,7 +662,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
662
662
|
* the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
|
|
663
663
|
* `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
|
|
664
664
|
*/
|
|
665
|
-
|
|
665
|
+
private validateRequestUserData;
|
|
666
666
|
useState<State extends Dictionary = Dictionary>(defaultValue?: State): Promise<State>;
|
|
667
667
|
protected getPendingRequestCountApproximation(): Promise<number>;
|
|
668
668
|
protected calculateEnqueuedRequestLimit(explicitLimit?: number): Promise<number | undefined>;
|
|
@@ -708,19 +708,19 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
708
708
|
protected _throwOnBlockedRequest(statusCode: number): void;
|
|
709
709
|
private isAllowedBasedOnRobotsTxtFile;
|
|
710
710
|
protected getRobotsTxtFileForUrl(url: string): Promise<RobotsTxtFile | undefined>;
|
|
711
|
-
|
|
711
|
+
private pauseOnMigration;
|
|
712
712
|
/**
|
|
713
713
|
* Fetches the next request to process from the underlying request provider.
|
|
714
714
|
*/
|
|
715
|
-
|
|
715
|
+
private fetchNextRequest;
|
|
716
716
|
/**
|
|
717
717
|
* Delays processing of the request based on the `sameDomainDelaySecs` option,
|
|
718
718
|
* adding it back to the queue after the timeout passes. Returns `true` if the request
|
|
719
719
|
* should be ignored and will be reclaimed to the queue once ready.
|
|
720
720
|
*/
|
|
721
|
-
|
|
721
|
+
private delayRequest;
|
|
722
722
|
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
|
723
|
-
|
|
723
|
+
private handleRequest;
|
|
724
724
|
/**
|
|
725
725
|
* Wrapper around the crawling context's `enqueueLinks` method:
|
|
726
726
|
* - Injects `crawlDepth` to each request being added based on the crawling context request.
|
|
@@ -738,15 +738,15 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
738
738
|
* Run async callback with given timeout and retry. Returns the result of the callback.
|
|
739
739
|
* @ignore
|
|
740
740
|
*/
|
|
741
|
-
|
|
741
|
+
private timeoutAndRetry;
|
|
742
742
|
/**
|
|
743
743
|
* Returns true if either RequestList or RequestQueue have a request ready for processing.
|
|
744
744
|
*/
|
|
745
|
-
|
|
745
|
+
private isTaskReadyFunction;
|
|
746
746
|
/**
|
|
747
747
|
* Returns true if both RequestList and RequestQueue have all requests finished.
|
|
748
748
|
*/
|
|
749
|
-
|
|
749
|
+
private defaultIsFinishedFunction;
|
|
750
750
|
/**
|
|
751
751
|
* Unwraps errors thrown by the context pipeline to get the actual user error.
|
|
752
752
|
* RequestHandlerError and ContextPipelineInitializationError wrap the actual error.
|
|
@@ -757,16 +757,15 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
757
757
|
*
|
|
758
758
|
* @param request The request object, passed separately to circumvent potential dynamic logic in crawlingContext.request
|
|
759
759
|
*/
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
protected _handleFailedRequestHandler(crawlingContext: CrawlingContext, error: Error): Promise<void>;
|
|
760
|
+
private requestFunctionErrorHandler;
|
|
761
|
+
private handleFailedRequestHandler;
|
|
763
762
|
/**
|
|
764
763
|
* Resolves the most verbose error message from a thrown error
|
|
765
764
|
* @param error The error received
|
|
766
765
|
* @returns The message to be logged
|
|
767
766
|
*/
|
|
768
767
|
protected _getMessageFromError(error: Error, forceStack?: boolean): string | TimeoutError | undefined;
|
|
769
|
-
|
|
768
|
+
private canRequestBeRetried;
|
|
770
769
|
/**
|
|
771
770
|
* Stops the crawler immediately.
|
|
772
771
|
*
|
|
@@ -93,7 +93,7 @@ export class BasicCrawler {
|
|
|
93
93
|
*/
|
|
94
94
|
autoscaledPool;
|
|
95
95
|
/**
|
|
96
|
-
* A reference to the underlying {@link
|
|
96
|
+
* A reference to the underlying {@link IProxyConfiguration} instance that manages the crawler's proxies.
|
|
97
97
|
* Only available if used by the crawler.
|
|
98
98
|
*/
|
|
99
99
|
proxyConfiguration;
|
|
@@ -144,11 +144,6 @@ export class BasicCrawler {
|
|
|
144
144
|
get handledRequestsCount() {
|
|
145
145
|
return this.stats.state.requestsFinished + this.stats.state.requestsFailed;
|
|
146
146
|
}
|
|
147
|
-
/** @deprecated Setting `handledRequestsCount` directly is no longer supported. The count is now derived from `this.stats`. */
|
|
148
|
-
set handledRequestsCount(_value) {
|
|
149
|
-
throw new Error('Setting `handledRequestsCount` directly is no longer supported. ' +
|
|
150
|
-
'The count is now derived from `this.stats.state.requestsFinished` and `this.stats.state.requestsFailed`.');
|
|
151
|
-
}
|
|
152
147
|
statusMessageLoggingInterval;
|
|
153
148
|
statusMessageCallback;
|
|
154
149
|
blockedStatusCodes = new Set();
|
|
@@ -357,7 +352,7 @@ export class BasicCrawler {
|
|
|
357
352
|
// (e.g., doesn't match enqueue strategy after redirect). Just return gracefully.
|
|
358
353
|
if (error instanceof ContextPipelineInterruptedError) {
|
|
359
354
|
this.stats.discardJob(request.id || request.uniqueKey);
|
|
360
|
-
await this.
|
|
355
|
+
await this.timeoutAndRetry(async () => this.requestManager?.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${crawlingContext.request.url} (${crawlingContext.request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
361
356
|
return;
|
|
362
357
|
}
|
|
363
358
|
// If the error happened during pipeline initialization (e.g., navigation timeout, session/proxy error,
|
|
@@ -365,8 +360,8 @@ export class BasicCrawler {
|
|
|
365
360
|
const isPipelineError = error instanceof ContextPipelineInitializationError || error instanceof SessionError;
|
|
366
361
|
if (isPipelineError) {
|
|
367
362
|
const unwrappedError = this.unwrapError(error);
|
|
368
|
-
await this.
|
|
369
|
-
// SessionError already retired the session in `
|
|
363
|
+
await this.requestFunctionErrorHandler(unwrappedError, crawlingContext, request, this.requestManager);
|
|
364
|
+
// SessionError already retired the session in `requestFunctionErrorHandler`;
|
|
370
365
|
// skip `markBad` to avoid double-counting usage/error score.
|
|
371
366
|
if (!(unwrappedError instanceof SessionError)) {
|
|
372
367
|
crawlingContext.session?.markBad();
|
|
@@ -392,7 +387,7 @@ export class BasicCrawler {
|
|
|
392
387
|
'Ongoing requests will be allowed to complete.');
|
|
393
388
|
return false;
|
|
394
389
|
}
|
|
395
|
-
return isTaskReadyFunction ? await isTaskReadyFunction() : await this.
|
|
390
|
+
return isTaskReadyFunction ? await isTaskReadyFunction() : await this.isTaskReadyFunction();
|
|
396
391
|
},
|
|
397
392
|
isFinishedFunction: async () => {
|
|
398
393
|
if (isMaxPagesExceeded()) {
|
|
@@ -407,7 +402,7 @@ export class BasicCrawler {
|
|
|
407
402
|
}
|
|
408
403
|
const isFinished = isFinishedFunction
|
|
409
404
|
? await isFinishedFunction()
|
|
410
|
-
: await this.
|
|
405
|
+
: await this.defaultIsFinishedFunction();
|
|
411
406
|
if (isFinished) {
|
|
412
407
|
const reason = isFinishedFunction
|
|
413
408
|
? "Crawler's custom isFinishedFunction() returned true, the crawler will shut down."
|
|
@@ -481,7 +476,7 @@ export class BasicCrawler {
|
|
|
481
476
|
};
|
|
482
477
|
}
|
|
483
478
|
async resolveRequest() {
|
|
484
|
-
const request = await this.
|
|
479
|
+
const request = await this.timeoutAndRetry(this.fetchNextRequest.bind(this), this.internalTimeoutMillis, `Fetching next request timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
485
480
|
// Reset loadedUrl so an old one is not carried over to retries.
|
|
486
481
|
if (request) {
|
|
487
482
|
request.loadedUrl = undefined;
|
|
@@ -489,7 +484,7 @@ export class BasicCrawler {
|
|
|
489
484
|
return request;
|
|
490
485
|
}
|
|
491
486
|
async resolveSession({ request }) {
|
|
492
|
-
const session = await this.
|
|
487
|
+
const session = await this.timeoutAndRetry(async () => {
|
|
493
488
|
const existingSession = await this.sessionPool.getSession(request.sessionId);
|
|
494
489
|
if (!existingSession) {
|
|
495
490
|
throw new ContextPipelineInitializationError(new MissingSessionError(request.sessionId));
|
|
@@ -512,13 +507,27 @@ export class BasicCrawler {
|
|
|
512
507
|
return { enqueueLinks: enqueueLinksWrapper, addRequests, sendRequest };
|
|
513
508
|
}
|
|
514
509
|
buildFinalContextPipeline() {
|
|
515
|
-
|
|
510
|
+
const subclassPipeline = (this.contextPipelineOptions.contextPipelineBuilder?.() ??
|
|
516
511
|
this.buildContextPipeline());
|
|
512
|
+
// `extendContext` runs *before* the subclass navigation pipeline (which includes the
|
|
513
|
+
// pre/post-navigation hooks). This makes the extension visible to those hooks and to the
|
|
514
|
+
// request handler alike. The trade-off is that `extendContext` cannot access
|
|
515
|
+
// navigation-dependent context members (e.g. `page`, `response`, `$`, `body`), as those
|
|
516
|
+
// don't exist yet at this point in the pipeline.
|
|
517
|
+
// The `extendContext` output (`ContextExtension`) is carried through the subclass pipeline at
|
|
518
|
+
// runtime (the pipeline copies each middleware's returned members onto the shared context), but
|
|
519
|
+
// TypeScript cannot express that `Context` transitively includes `ContextExtension` here. The
|
|
520
|
+
// casts below are sound because `buildFinalContextPipeline` is declared to return the fully
|
|
521
|
+
// resolved `ExtendedContext` (= `Context & ContextExtension`).
|
|
517
522
|
const { extendContext } = this.contextPipelineOptions;
|
|
523
|
+
let contextPipeline;
|
|
518
524
|
if (extendContext !== undefined) {
|
|
519
|
-
contextPipeline =
|
|
520
|
-
action: async (context) => await extendContext(context)
|
|
521
|
-
|
|
525
|
+
contextPipeline = ContextPipeline.create()
|
|
526
|
+
.compose({ action: async (context) => await extendContext(context) })
|
|
527
|
+
.chain(subclassPipeline);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
contextPipeline = subclassPipeline;
|
|
522
531
|
}
|
|
523
532
|
contextPipeline = contextPipeline.compose({
|
|
524
533
|
action: async (context) => {
|
|
@@ -654,11 +663,11 @@ export class BasicCrawler {
|
|
|
654
663
|
this.setStatusMessage('Starting the crawler.', { level: 'INFO' });
|
|
655
664
|
const sigintHandler = async () => {
|
|
656
665
|
this.log.warning('Pausing... Press CTRL+C again to force exit. To resume, do: CRAWLEE_PURGE_ON_START=0 npm start');
|
|
657
|
-
await this.
|
|
666
|
+
await this.pauseOnMigration();
|
|
658
667
|
await this.autoscaledPool.abort();
|
|
659
668
|
};
|
|
660
669
|
// Attach a listener to handle migration and aborting events gracefully.
|
|
661
|
-
const boundPauseOnMigration = this.
|
|
670
|
+
const boundPauseOnMigration = this.pauseOnMigration.bind(this);
|
|
662
671
|
process.once('SIGINT', sigintHandler);
|
|
663
672
|
const eventManager = serviceLocator.getEventManager();
|
|
664
673
|
eventManager.on("migrating" /* EventType.MIGRATING */, boundPauseOnMigration);
|
|
@@ -1021,7 +1030,7 @@ export class BasicCrawler {
|
|
|
1021
1030
|
return undefined;
|
|
1022
1031
|
}
|
|
1023
1032
|
}
|
|
1024
|
-
async
|
|
1033
|
+
async pauseOnMigration() {
|
|
1025
1034
|
if (this.autoscaledPool) {
|
|
1026
1035
|
// if run wasn't called, this is going to crash
|
|
1027
1036
|
await this.autoscaledPool.pause(SAFE_MIGRATION_WAIT_MILLIS).catch((err) => {
|
|
@@ -1058,9 +1067,9 @@ export class BasicCrawler {
|
|
|
1058
1067
|
/**
|
|
1059
1068
|
* Fetches the next request to process from the underlying request provider.
|
|
1060
1069
|
*/
|
|
1061
|
-
async
|
|
1070
|
+
async fetchNextRequest() {
|
|
1062
1071
|
if (this.requestManager === undefined) {
|
|
1063
|
-
throw new Error(`
|
|
1072
|
+
throw new Error(`fetchNextRequest called on an uninitialized crawler`);
|
|
1064
1073
|
}
|
|
1065
1074
|
return this.requestManager.fetchNextRequest();
|
|
1066
1075
|
}
|
|
@@ -1095,7 +1104,7 @@ export class BasicCrawler {
|
|
|
1095
1104
|
try {
|
|
1096
1105
|
request.state = RequestState.REQUEST_HANDLER;
|
|
1097
1106
|
await this.runRequestHandler(crawlingContext);
|
|
1098
|
-
await this.
|
|
1107
|
+
await this.timeoutAndRetry(async () => requestSource.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
1099
1108
|
isRequestLocked = false; // markRequestAsHandled succeeded and unlocked the request
|
|
1100
1109
|
this.stats.finishJob(statisticsId, request.retryCount);
|
|
1101
1110
|
// reclaim session if request finishes successfully
|
|
@@ -1106,9 +1115,9 @@ export class BasicCrawler {
|
|
|
1106
1115
|
const err = this.unwrapError(rawError);
|
|
1107
1116
|
try {
|
|
1108
1117
|
request.state = RequestState.ERROR_HANDLER;
|
|
1109
|
-
await addTimeoutToPromise(async () => this.
|
|
1118
|
+
await addTimeoutToPromise(async () => this.requestFunctionErrorHandler(err, crawlingContext, request, requestSource), this.internalTimeoutMillis, `Handling request failure of ${request.url} (${request.id}) timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
1110
1119
|
if (!(err instanceof CriticalError)) {
|
|
1111
|
-
isRequestLocked = false; //
|
|
1120
|
+
isRequestLocked = false; // requestFunctionErrorHandler calls either markRequestAsHandled or reclaimRequest
|
|
1112
1121
|
}
|
|
1113
1122
|
request.state = RequestState.DONE;
|
|
1114
1123
|
}
|
|
@@ -1211,7 +1220,7 @@ export class BasicCrawler {
|
|
|
1211
1220
|
* Run async callback with given timeout and retry. Returns the result of the callback.
|
|
1212
1221
|
* @ignore
|
|
1213
1222
|
*/
|
|
1214
|
-
async
|
|
1223
|
+
async timeoutAndRetry(handler, timeout, error, maxRetries = 3, retried = 1) {
|
|
1215
1224
|
try {
|
|
1216
1225
|
return await addTimeoutToPromise(handler, timeout, error);
|
|
1217
1226
|
}
|
|
@@ -1219,7 +1228,7 @@ export class BasicCrawler {
|
|
|
1219
1228
|
if (retried <= maxRetries) {
|
|
1220
1229
|
// we retry on any error, not just timeout
|
|
1221
1230
|
this.log.warning(`${e.message} (retrying ${retried}/${maxRetries})`);
|
|
1222
|
-
return this.
|
|
1231
|
+
return this.timeoutAndRetry(handler, timeout, error, maxRetries, retried + 1);
|
|
1223
1232
|
}
|
|
1224
1233
|
throw e;
|
|
1225
1234
|
}
|
|
@@ -1227,13 +1236,13 @@ export class BasicCrawler {
|
|
|
1227
1236
|
/**
|
|
1228
1237
|
* Returns true if either RequestList or RequestQueue have a request ready for processing.
|
|
1229
1238
|
*/
|
|
1230
|
-
async
|
|
1239
|
+
async isTaskReadyFunction() {
|
|
1231
1240
|
return this.requestManager !== undefined && !(await this.requestManager.isEmpty());
|
|
1232
1241
|
}
|
|
1233
1242
|
/**
|
|
1234
1243
|
* Returns true if both RequestList and RequestQueue have all requests finished.
|
|
1235
1244
|
*/
|
|
1236
|
-
async
|
|
1245
|
+
async defaultIsFinishedFunction() {
|
|
1237
1246
|
return !this.requestManager || (await this.requestManager.isFinished());
|
|
1238
1247
|
}
|
|
1239
1248
|
/**
|
|
@@ -1253,12 +1262,12 @@ export class BasicCrawler {
|
|
|
1253
1262
|
*
|
|
1254
1263
|
* @param request The request object, passed separately to circumvent potential dynamic logic in crawlingContext.request
|
|
1255
1264
|
*/
|
|
1256
|
-
async
|
|
1265
|
+
async requestFunctionErrorHandler(error, crawlingContext, request, source) {
|
|
1257
1266
|
request.pushErrorMessage(error);
|
|
1258
1267
|
if (error instanceof CriticalError) {
|
|
1259
1268
|
throw error;
|
|
1260
1269
|
}
|
|
1261
|
-
const shouldRetryRequest = this.
|
|
1270
|
+
const shouldRetryRequest = this.canRequestBeRetried(request, error);
|
|
1262
1271
|
if (shouldRetryRequest) {
|
|
1263
1272
|
await this.stats.errorTrackerRetry.addAsync(error, crawlingContext);
|
|
1264
1273
|
await this.errorHandler?.(crawlingContext, // valid cast - ExtendedContext transitively extends CrawlingContext
|
|
@@ -1299,18 +1308,9 @@ export class BasicCrawler {
|
|
|
1299
1308
|
// Mark the request as failed and do not retry.
|
|
1300
1309
|
await source.markRequestAsHandled(request);
|
|
1301
1310
|
this.stats.failJob(request.id || request.uniqueKey, request.retryCount);
|
|
1302
|
-
await this.
|
|
1303
|
-
}
|
|
1304
|
-
async _tagUserHandlerError(cb) {
|
|
1305
|
-
try {
|
|
1306
|
-
return (await cb());
|
|
1307
|
-
}
|
|
1308
|
-
catch (e) {
|
|
1309
|
-
Object.defineProperty(e, 'triggeredFromUserHandler', { value: true });
|
|
1310
|
-
throw e;
|
|
1311
|
-
}
|
|
1311
|
+
await this.handleFailedRequestHandler(crawlingContext, error); // This function prints an error message.
|
|
1312
1312
|
}
|
|
1313
|
-
async
|
|
1313
|
+
async handleFailedRequestHandler(crawlingContext, error) {
|
|
1314
1314
|
// Always log the last error regardless if the user provided a failedRequestHandler
|
|
1315
1315
|
const { id, url, method, uniqueKey } = crawlingContext.request;
|
|
1316
1316
|
const message = this._getMessageFromError(error, true);
|
|
@@ -1339,7 +1339,7 @@ export class BasicCrawler {
|
|
|
1339
1339
|
? (error.stack ?? [error.message || error, ...stackLines].join('\n'))
|
|
1340
1340
|
: [error.message || error, userLine].join('\n');
|
|
1341
1341
|
}
|
|
1342
|
-
|
|
1342
|
+
canRequestBeRetried(request, error) {
|
|
1343
1343
|
// Request should never be retried, or the error encountered makes it not able to be retried.
|
|
1344
1344
|
if (request.noRetry || error instanceof NonRetryableError) {
|
|
1345
1345
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/basic",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.82",
|
|
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"
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@apify/datastructures": "^2.0.0",
|
|
43
43
|
"@apify/timeout": "^0.3.2",
|
|
44
44
|
"@apify/utilities": "^2.15.5",
|
|
45
|
-
"@crawlee/core": "4.0.0-beta.
|
|
46
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
47
|
-
"@crawlee/types": "4.0.0-beta.
|
|
48
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
45
|
+
"@crawlee/core": "4.0.0-beta.82",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.82",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.82",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.82",
|
|
49
49
|
"csv-stringify": "^6.5.2",
|
|
50
50
|
"fs-extra": "^11.3.0",
|
|
51
51
|
"ow": "^2.0.0",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"type-fest": "^4.41.0"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@crawlee/impit-client": "^4.0.0-beta.
|
|
57
|
+
"@crawlee/impit-client": "^4.0.0-beta.82"
|
|
58
58
|
},
|
|
59
59
|
"lerna": {
|
|
60
60
|
"command": {
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "eb1096f7c7743d124375ef011fbbadb19476822e"
|
|
67
67
|
}
|