@crawlee/basic 4.0.0-beta.207 → 4.0.0-beta.208
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 +4 -4
- package/internals/basic-crawler.js +83 -90
- package/internals/crawlers/context_pipeline.d.ts +11 -7
- package/internals/crawlers/context_pipeline.js +28 -36
- package/internals/errors.d.ts +5 -0
- package/internals/errors.js +7 -0
- package/internals/request-timeout.d.ts +6 -0
- package/internals/request-timeout.js +27 -2
- package/internals/session_pool/session.d.ts +2 -1
- package/internals/session_pool/session.js +6 -3
- package/package.json +7 -7
|
@@ -684,8 +684,8 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
684
684
|
protected runRequestHandler(crawlingContext: ExtendedContext): Promise<void>;
|
|
685
685
|
/**
|
|
686
686
|
* Runs `callback` inside a {@link StorageTransaction}, unless transactional storage is disabled.
|
|
687
|
-
* Deliberately does **not** commit on return -
|
|
688
|
-
*
|
|
687
|
+
* Deliberately does **not** commit on return - the pipeline hands failures to `handleRequestFailure`, so a
|
|
688
|
+
* normal return says nothing about success. `handleRequest` commits on success.
|
|
689
689
|
*/
|
|
690
690
|
private runInStorageTransaction;
|
|
691
691
|
/**
|
|
@@ -710,7 +710,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
710
710
|
* Fetches the next request to process from the underlying request provider.
|
|
711
711
|
*/
|
|
712
712
|
private fetchNextRequest;
|
|
713
|
-
/**
|
|
713
|
+
/** Runs the request handler and marks the request as handled. Failures are left to `handleRequestFailure`. */
|
|
714
714
|
private handleRequest;
|
|
715
715
|
/**
|
|
716
716
|
* Generator function that yields requests injected with the given crawl depth.
|
|
@@ -729,7 +729,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
729
729
|
private isTaskReadyFunction;
|
|
730
730
|
/**
|
|
731
731
|
* Unwraps errors thrown by the context pipeline to get the actual user error.
|
|
732
|
-
* RequestHandlerError and ContextPipelineCleanupError wrap the actual error.
|
|
732
|
+
* RequestHandlerError, ContextPipelineInitializationError and ContextPipelineCleanupError wrap the actual error.
|
|
733
733
|
*/
|
|
734
734
|
private unwrapError;
|
|
735
735
|
/**
|
|
@@ -13,12 +13,12 @@ import { ConcurrencySystem } from './autoscaling/concurrency_system.js';
|
|
|
13
13
|
import { mergeCookies } from './cookie_utils.js';
|
|
14
14
|
import { ContextPipeline } from './crawlers/context_pipeline.js';
|
|
15
15
|
import { CrawlingRequest, RequestState } from './crawling_request.js';
|
|
16
|
-
import { ContextPipelineCleanupError, ContextPipelineInterruptedError, MissingSessionError, NavigationSkippedError, PersistentRateLimitError, RequestHandlerError, RequestThrottledError, RetryRequestError, } from './errors.js';
|
|
16
|
+
import { ContextPipelineCleanupError, ContextPipelineInterruptedError, MissingSessionError, NavigationSkippedError, PersistentRateLimitError, ContextPipelineInitializationError, RequestHandlerError, RequestThrottledError, RetryRequestError, } from './errors.js';
|
|
17
17
|
import { Statistics } from './crawlers/statistics.js';
|
|
18
18
|
import { applyRequestTransform, buildEnqueueStrategyPatterns, constructUrlPatternObjects, createRequestOptions, createSkippedRequestArgs, filterRequestOptionsByPatterns, } from './enqueue_links/index.js';
|
|
19
19
|
import { parseRetryAfterHeader } from './http.js';
|
|
20
20
|
import { CrawlerRun } from './crawler-run.js';
|
|
21
|
-
import { extendTimeoutKey, navigationDeadlineKey, raceWithTimeout, timeoutExpiredKey, } from './request-timeout.js';
|
|
21
|
+
import { extendTimeoutKey, navigationDeadlineKey, raceWithTimeout, suspendTimeoutKey, timeoutExpiredKey, } from './request-timeout.js';
|
|
22
22
|
import { createSendRequest } from './send-request.js';
|
|
23
23
|
import { Router, validateUserData } from './router.js';
|
|
24
24
|
import { BLOCKED_STATUS_CODES } from './session_pool/consts.js';
|
|
@@ -462,44 +462,19 @@ export class BasicCrawler {
|
|
|
462
462
|
// matching `recordRequestStart`.
|
|
463
463
|
this.statistics.recordRequestStart(request.id || request.uniqueKey);
|
|
464
464
|
const crawlingContext = { request };
|
|
465
|
-
// Runs inside the pipeline call, so the deferred cleanups registered by earlier middlewares
|
|
466
|
-
// (e.g. closing the page) only fire after the error handler has seen the partial context.
|
|
467
|
-
const handleInitializationError = async (rawError) => {
|
|
468
|
-
// Roll back *before* the error handler runs, for the same reason `handleRequest` does.
|
|
469
|
-
currentStorageTransaction()?.rollback();
|
|
470
|
-
// ContextPipelineInterruptedError means the request was intentionally skipped
|
|
471
|
-
// (e.g., doesn't match enqueue strategy after redirect). Just return gracefully.
|
|
472
|
-
if (rawError instanceof ContextPipelineInterruptedError) {
|
|
473
|
-
this.statistics.discardRequestRecord(request.id || request.uniqueKey);
|
|
474
|
-
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.`);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
// An error during pipeline initialization (e.g., navigation timeout, session/proxy error,
|
|
478
|
-
// i.e. not in user's requestHandler) goes through the normal error flow.
|
|
479
|
-
const error = this.unwrapError(rawError);
|
|
480
|
-
await this.requestFunctionErrorHandler(error, crawlingContext, request, this.requestManager);
|
|
481
|
-
// SessionError already retired the session in `requestFunctionErrorHandler`;
|
|
482
|
-
// skip `markBad` to avoid double-counting usage/error score.
|
|
483
|
-
if (!this.errorAbsolvesSession(error)) {
|
|
484
|
-
crawlingContext.session?.markBad();
|
|
485
|
-
}
|
|
486
|
-
};
|
|
487
465
|
try {
|
|
488
466
|
// The transaction spans the whole pipeline call, covering the navigation hooks
|
|
489
|
-
// and `extendContext` too; `handleRequest`
|
|
467
|
+
// and `extendContext` too; `handleRequest` commits it and `handleRequestFailure` rolls it back.
|
|
490
468
|
await this.runInStorageTransaction(async () =>
|
|
491
469
|
// Navigation, the navigation hooks and the request handler are timed individually, but the
|
|
492
470
|
// phases between them are not, so a request could still get stuck indefinitely. This is the
|
|
493
471
|
// catch-all for that - see `raceWithTimeout` for why it is a bare timer, not a timeout frame.
|
|
494
|
-
await this.withRequestTimeout(crawlingContext, this.#basicPipeline
|
|
495
|
-
.chain(this.contextPipeline)
|
|
496
|
-
.call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request), handleInitializationError)));
|
|
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))));
|
|
497
473
|
}
|
|
498
474
|
catch (error) {
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
if (error instanceof SessionError || error instanceof TimeoutError) {
|
|
475
|
+
// The internal timeout above fired, or `handleRequestFailure` threw a `SessionError` - both
|
|
476
|
+
// take the normal error flow. Anything else is fatal.
|
|
477
|
+
if (error instanceof SessionError || crawlingContext[timeoutExpiredKey]?.()) {
|
|
503
478
|
const unwrappedError = this.unwrapError(error);
|
|
504
479
|
await this.requestFunctionErrorHandler(unwrappedError, crawlingContext, request, this.requestManager);
|
|
505
480
|
if (!this.errorAbsolvesSession(unwrappedError)) {
|
|
@@ -1373,8 +1348,8 @@ export class BasicCrawler {
|
|
|
1373
1348
|
}
|
|
1374
1349
|
/**
|
|
1375
1350
|
* Runs `callback` inside a {@link StorageTransaction}, unless transactional storage is disabled.
|
|
1376
|
-
* Deliberately does **not** commit on return -
|
|
1377
|
-
*
|
|
1351
|
+
* Deliberately does **not** commit on return - the pipeline hands failures to `handleRequestFailure`, so a
|
|
1352
|
+
* normal return says nothing about success. `handleRequest` commits on success.
|
|
1378
1353
|
*/
|
|
1379
1354
|
async runInStorageTransaction(callback) {
|
|
1380
1355
|
if (!this.#transactionalStorageEnabled) {
|
|
@@ -1392,7 +1367,7 @@ export class BasicCrawler {
|
|
|
1392
1367
|
}
|
|
1393
1368
|
finally {
|
|
1394
1369
|
if (transaction.state === 'open') {
|
|
1395
|
-
// `handleRequest` commits or rolls back on every normal path, so an open transaction on
|
|
1370
|
+
// `handleRequest` commits or `handleRequestFailure` rolls back on every normal path, so an open transaction on
|
|
1396
1371
|
// a normal return is a wiring bug; on a propagating throw (a pipeline-level failure) it is
|
|
1397
1372
|
// expected. Either way, discard the unvalidated writes; only the former is worth flagging.
|
|
1398
1373
|
if (!threw) {
|
|
@@ -1515,7 +1490,7 @@ export class BasicCrawler {
|
|
|
1515
1490
|
}
|
|
1516
1491
|
return this.requestManager.fetchNextRequest();
|
|
1517
1492
|
}
|
|
1518
|
-
/**
|
|
1493
|
+
/** Runs the request handler and marks the request as handled. Failures are left to `handleRequestFailure`. */
|
|
1519
1494
|
// oxlint-disable-next-line crawlee/prefer-private-fields -- patched by @crawlee/otel
|
|
1520
1495
|
async handleRequest(crawlingContext, requestSource, request) {
|
|
1521
1496
|
// An earlier phase we cannot cancel (e.g. a slow `extendContext`) may have run past the internal timeout,
|
|
@@ -1524,68 +1499,84 @@ export class BasicCrawler {
|
|
|
1524
1499
|
if (crawlingContext[timeoutExpiredKey]?.()) {
|
|
1525
1500
|
return;
|
|
1526
1501
|
}
|
|
1527
|
-
|
|
1502
|
+
request.state = RequestState.REQUEST_HANDLER;
|
|
1503
|
+
await this.runRequestHandler(crawlingContext);
|
|
1504
|
+
// Commit *before* marking the request as handled, so a commit failure fails the request and
|
|
1505
|
+
// it is retried. This also closes the transaction, so everything below passes through.
|
|
1528
1506
|
// Opened by `runInStorageTransaction`; absent when disabled or when the subclass opens its own.
|
|
1529
|
-
|
|
1530
|
-
|
|
1507
|
+
await currentStorageTransaction()?.commit();
|
|
1508
|
+
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
|
+
this.statistics.recordRequestSuccess(request.id || request.uniqueKey, request.retryCount);
|
|
1510
|
+
// reclaim session if request finishes successfully
|
|
1511
|
+
request.state = RequestState.DONE;
|
|
1512
|
+
crawlingContext.session.markGood();
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Handles a request that failed anywhere in the context pipeline - a middleware (e.g. navigation or its hooks)
|
|
1516
|
+
* or `handleRequest`. Runs inside the pipeline call, so the cleanups (e.g. closing the page) only fire after the
|
|
1517
|
+
* error handler has seen the context.
|
|
1518
|
+
*/
|
|
1519
|
+
async #handleRequestFailure(rawError, crawlingContext, requestSource, request) {
|
|
1520
|
+
// Roll back *before* any error handler runs - error handlers write to real storage precisely
|
|
1521
|
+
// because the transaction is already closed. A no-op when the commit in `handleRequest` succeeded.
|
|
1522
|
+
currentStorageTransaction()?.rollback();
|
|
1523
|
+
const skipped = rawError instanceof ContextPipelineInitializationError &&
|
|
1524
|
+
rawError.cause instanceof ContextPipelineInterruptedError;
|
|
1525
|
+
const err = this.unwrapError(rawError);
|
|
1526
|
+
const context = crawlingContext;
|
|
1527
|
+
const retryCountBefore = request.retryCount;
|
|
1528
|
+
const resumeRequestTimeout = crawlingContext[suspendTimeoutKey]?.();
|
|
1531
1529
|
try {
|
|
1532
|
-
request
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1530
|
+
// ContextPipelineInterruptedError means the request was intentionally skipped
|
|
1531
|
+
// (e.g., doesn't match enqueue strategy after redirect). Just return gracefully.
|
|
1532
|
+
if (skipped) {
|
|
1533
|
+
this.statistics.discardRequestRecord(request.id || request.uniqueKey);
|
|
1534
|
+
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
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
request.state = RequestState.ERROR_HANDLER;
|
|
1538
|
+
await addTimeoutToPromise(async () => this.requestFunctionErrorHandler(err, context, request, requestSource), this.internalTimeoutMillis, `Handling request failure of ${request.url} (${request.id}) timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
1541
1539
|
request.state = RequestState.DONE;
|
|
1542
|
-
crawlingContext.session.markGood();
|
|
1543
1540
|
}
|
|
1544
|
-
catch (
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
if (!
|
|
1553
|
-
|
|
1541
|
+
catch (secondaryError) {
|
|
1542
|
+
const unwrappedSecondaryError = this.unwrapError(secondaryError);
|
|
1543
|
+
const timedOut = unwrappedSecondaryError instanceof TimeoutError;
|
|
1544
|
+
request.state = RequestState.ERROR;
|
|
1545
|
+
if (timedOut) {
|
|
1546
|
+
// Not worth crashing the crawler over. The request's state is unknown, so it gets retried - and the
|
|
1547
|
+
// retry is counted, so that an error handler that always times out does not retry it forever.
|
|
1548
|
+
this.log.warning(unwrappedSecondaryError.message);
|
|
1549
|
+
if (!skipped && request.retryCount === retryCountBefore) {
|
|
1550
|
+
request.retryCount++;
|
|
1554
1551
|
}
|
|
1555
|
-
request.state = RequestState.DONE;
|
|
1556
1552
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
request.
|
|
1569
|
-
throw unwrappedSecondaryError;
|
|
1553
|
+
else if (!(unwrappedSecondaryError instanceof CriticalError)) {
|
|
1554
|
+
// avoid reprinting the same critical error multiple times, as it will be printed by Nodejs at the end anyway
|
|
1555
|
+
const apifySpecific = process.env.APIFY_IS_AT_HOME
|
|
1556
|
+
? `This may have happened due to an internal error of Apify's API or due to a misconfigured crawler.`
|
|
1557
|
+
: '';
|
|
1558
|
+
this.log.exception(unwrappedSecondaryError, 'An exception occurred during handling of failed request. ' +
|
|
1559
|
+
`This places the crawler and its underlying storages into an unknown state and crawling will be terminated. ${apifySpecific}`);
|
|
1560
|
+
}
|
|
1561
|
+
// Safety net - the error handler neither marked the request as handled nor reclaimed it (e.g. after a
|
|
1562
|
+
// CriticalError). Reclaiming a request that is no longer in progress is a harmless no-op on the storage backend.
|
|
1563
|
+
try {
|
|
1564
|
+
await addTimeoutToPromise(async () => requestSource.reclaimRequest(request), this.internalTimeoutMillis, `Reclaiming request ${request.url} (${request.id}) timed out.`);
|
|
1570
1565
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1566
|
+
catch {
|
|
1567
|
+
// The request was never in progress, or could not be reclaimed. Either way it's fine.
|
|
1568
|
+
}
|
|
1569
|
+
if (!timedOut) {
|
|
1570
|
+
throw unwrappedSecondaryError;
|
|
1575
1571
|
}
|
|
1576
1572
|
}
|
|
1577
1573
|
finally {
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
}
|
|
1585
|
-
catch {
|
|
1586
|
-
// The request was never in progress, or could not be reclaimed. Either way it's fine.
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1574
|
+
resumeRequestTimeout?.();
|
|
1575
|
+
}
|
|
1576
|
+
// decrease the session score if the request fails (but the error handler did not throw);
|
|
1577
|
+
// skip when the error is a SessionError, which already retired the session
|
|
1578
|
+
if (!skipped && !this.errorAbsolvesSession(err)) {
|
|
1579
|
+
context.session?.markBad();
|
|
1589
1580
|
}
|
|
1590
1581
|
}
|
|
1591
1582
|
/**
|
|
@@ -1641,10 +1632,12 @@ export class BasicCrawler {
|
|
|
1641
1632
|
}
|
|
1642
1633
|
/**
|
|
1643
1634
|
* Unwraps errors thrown by the context pipeline to get the actual user error.
|
|
1644
|
-
* RequestHandlerError and ContextPipelineCleanupError wrap the actual error.
|
|
1635
|
+
* RequestHandlerError, ContextPipelineInitializationError and ContextPipelineCleanupError wrap the actual error.
|
|
1645
1636
|
*/
|
|
1646
1637
|
unwrapError(error) {
|
|
1647
|
-
if (error instanceof RequestHandlerError ||
|
|
1638
|
+
if (error instanceof RequestHandlerError ||
|
|
1639
|
+
error instanceof ContextPipelineInitializationError ||
|
|
1640
|
+
error instanceof ContextPipelineCleanupError) {
|
|
1648
1641
|
return this.unwrapError(error.cause);
|
|
1649
1642
|
}
|
|
1650
1643
|
return error;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Awaitable } from '@crawlee/types';
|
|
2
2
|
/**
|
|
3
|
-
* Registers a callback that runs after the final context consumer finishes.
|
|
4
|
-
*
|
|
3
|
+
* Registers a callback that runs after the final context consumer finishes. Cleanups run in reverse registration
|
|
4
|
+
* order and receive the same `error` as the `onError` parameter of {@link ContextPipeline.call}: `undefined` on
|
|
5
|
+
* success, otherwise a {@link ContextPipelineInitializationError} (a middleware failed) or a
|
|
6
|
+
* {@link RequestHandlerError} (the consumer failed), with the original in `cause`. A cleanup that throws fails the
|
|
7
|
+
* whole call with a {@link ContextPipelineCleanupError}, which is critical.
|
|
5
8
|
*/
|
|
6
9
|
export type CleanupRegistrar = (cleanup: (error?: unknown) => Awaitable<void>) => void;
|
|
7
10
|
/**
|
|
@@ -60,12 +63,13 @@ export declare abstract class ContextPipeline<TContextBase, TCrawlingContext ext
|
|
|
60
63
|
*
|
|
61
64
|
* @param crawlingContext - The initial context to process through the pipeline
|
|
62
65
|
* @param finalContextConsumer - The function that will receive the final enhanced context
|
|
63
|
-
* @param
|
|
64
|
-
*
|
|
66
|
+
* @param onError - Receives a middleware failure wrapped in a {@link ContextPipelineInitializationError}, or a
|
|
67
|
+
* consumer failure wrapped in a {@link RequestHandlerError}, with the original in `cause`. After a middleware
|
|
68
|
+
* failure, the rest of the chain and the consumer are skipped. Runs before the cleanups, so it still sees
|
|
69
|
+
* whatever the middlewares set up.
|
|
65
70
|
*
|
|
66
|
-
* @throws {RequestHandlerError} When the final context consumer throws an exception
|
|
67
71
|
* @throws {ContextPipelineCleanupError} When cleanup operations fail
|
|
68
|
-
* @throws
|
|
72
|
+
* @throws Whatever `onError` throws
|
|
69
73
|
*/
|
|
70
|
-
abstract call(crawlingContext: TContextBase, finalContextConsumer: (finalContext: TCrawlingContext) => Awaitable<unknown>,
|
|
74
|
+
abstract call(crawlingContext: TContextBase, finalContextConsumer: (finalContext: TCrawlingContext) => Awaitable<unknown>, onError: (error: unknown) => Awaitable<void>): Promise<void>;
|
|
71
75
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { serviceLocator
|
|
2
|
-
import { ContextPipelineCleanupError, RequestHandlerError } from '../errors.js';
|
|
1
|
+
import { serviceLocator } from '@crawlee/core';
|
|
2
|
+
import { ContextPipelineCleanupError, ContextPipelineInitializationError, RequestHandlerError } from '../errors.js';
|
|
3
3
|
/**
|
|
4
4
|
* Encapsulates the logic of gradually enhancing the crawling context with additional information and utilities.
|
|
5
5
|
*
|
|
@@ -57,55 +57,47 @@ class ContextPipelineImpl extends ContextPipeline {
|
|
|
57
57
|
/**
|
|
58
58
|
* @inheritdoc
|
|
59
59
|
*/
|
|
60
|
-
async call(crawlingContext, finalContextConsumer,
|
|
60
|
+
async call(crawlingContext, finalContextConsumer, onError) {
|
|
61
61
|
const middlewares = Array.from(this.middlewareChain()).reverse();
|
|
62
62
|
const cleanupStack = [];
|
|
63
63
|
const onCleanup = (cleanup) => {
|
|
64
64
|
cleanupStack.push(cleanup);
|
|
65
65
|
};
|
|
66
|
-
let
|
|
66
|
+
let failure;
|
|
67
|
+
let consumerStarted = false;
|
|
67
68
|
try {
|
|
68
69
|
for (const middleware of middlewares) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Object.defineProperty(crawlingContext, key, Object.getOwnPropertyDescriptor(contextExtension, key));
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
serviceLocator
|
|
83
|
-
.getLogger()
|
|
84
|
-
.debug(`Context pipeline failed to define property ${key.toString()}:`, error);
|
|
70
|
+
const contextExtension = await middleware(crawlingContext, onCleanup);
|
|
71
|
+
const extensionNames = [
|
|
72
|
+
...Object.getOwnPropertyNames(contextExtension),
|
|
73
|
+
...Object.getOwnPropertySymbols(contextExtension),
|
|
74
|
+
];
|
|
75
|
+
for (const key of extensionNames) {
|
|
76
|
+
try {
|
|
77
|
+
if (Object.getOwnPropertyDescriptor(crawlingContext, key)?.configurable !== false) {
|
|
78
|
+
Object.defineProperty(crawlingContext, key, Object.getOwnPropertyDescriptor(contextExtension, key));
|
|
85
79
|
}
|
|
86
80
|
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
serviceLocator
|
|
83
|
+
.getLogger()
|
|
84
|
+
.debug(`Context pipeline failed to define property ${key.toString()}:`, error);
|
|
85
|
+
}
|
|
87
86
|
}
|
|
88
|
-
catch (exception) {
|
|
89
|
-
await onInitializationError(exception);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
await finalContextConsumer(crawlingContext);
|
|
95
|
-
}
|
|
96
|
-
catch (exception) {
|
|
97
|
-
if (exception instanceof SessionError) {
|
|
98
|
-
consumerException = exception;
|
|
99
|
-
throw exception; // Session errors are re-thrown as-is
|
|
100
|
-
}
|
|
101
|
-
consumerException = exception;
|
|
102
|
-
throw new RequestHandlerError(exception);
|
|
103
87
|
}
|
|
88
|
+
consumerStarted = true;
|
|
89
|
+
await finalContextConsumer(crawlingContext);
|
|
90
|
+
}
|
|
91
|
+
catch (exception) {
|
|
92
|
+
failure = consumerStarted
|
|
93
|
+
? new RequestHandlerError(exception)
|
|
94
|
+
: new ContextPipelineInitializationError(exception);
|
|
95
|
+
await onError(failure);
|
|
104
96
|
}
|
|
105
97
|
finally {
|
|
106
98
|
try {
|
|
107
99
|
for (const cleanup of cleanupStack.reverse()) {
|
|
108
|
-
await cleanup(
|
|
100
|
+
await cleanup(failure);
|
|
109
101
|
}
|
|
110
102
|
}
|
|
111
103
|
catch (exception) {
|
package/internals/errors.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export declare class ContextPipelineInterruptedError extends Error {
|
|
|
44
44
|
export declare class ContextPipelineCleanupError extends CriticalError {
|
|
45
45
|
constructor(error: unknown, options?: ErrorOptions);
|
|
46
46
|
}
|
|
47
|
+
/** Wraps a failure of a context pipeline middleware, telling it apart from a {@link RequestHandlerError}. */
|
|
48
|
+
export declare class ContextPipelineInitializationError extends Error {
|
|
49
|
+
constructor(error: unknown, options?: ErrorOptions);
|
|
50
|
+
}
|
|
51
|
+
/** Wraps a failure of the context pipeline's final consumer (the request handler), telling it apart from a middleware failure. */
|
|
47
52
|
export declare class RequestHandlerError extends Error {
|
|
48
53
|
constructor(error: unknown, options?: ErrorOptions);
|
|
49
54
|
}
|
package/internals/errors.js
CHANGED
|
@@ -54,6 +54,13 @@ export class ContextPipelineCleanupError extends CriticalError {
|
|
|
54
54
|
super(undefined, { cause: error, ...options });
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
/** Wraps a failure of a context pipeline middleware, telling it apart from a {@link RequestHandlerError}. */
|
|
58
|
+
export class ContextPipelineInitializationError extends Error {
|
|
59
|
+
constructor(error, options) {
|
|
60
|
+
super(undefined, { cause: error, ...options });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Wraps a failure of the context pipeline's final consumer (the request handler), telling it apart from a middleware failure. */
|
|
57
64
|
export class RequestHandlerError extends Error {
|
|
58
65
|
constructor(error, options) {
|
|
59
66
|
super(undefined, { cause: error, ...options });
|
|
@@ -16,10 +16,16 @@ export declare const extendTimeoutKey: unique symbol;
|
|
|
16
16
|
* bail, so a request whose timeout already elapsed does not carry on after the crawler moved past it.
|
|
17
17
|
*/
|
|
18
18
|
export declare const timeoutExpiredKey: unique symbol;
|
|
19
|
+
/**
|
|
20
|
+
* Stops the internal request timeout's clock until the returned callback is called. Error handling has a timeout
|
|
21
|
+
* of its own, and must not be cut short (and handled again) by the request timeout.
|
|
22
|
+
*/
|
|
23
|
+
export declare const suspendTimeoutKey: unique symbol;
|
|
19
24
|
/** The slots the internal request timeout hangs on the in-flight crawling context. */
|
|
20
25
|
export interface RequestTimeoutContext {
|
|
21
26
|
[extendTimeoutKey]?: (extraMillis: number) => void;
|
|
22
27
|
[timeoutExpiredKey]?: () => boolean;
|
|
28
|
+
[suspendTimeoutKey]?: () => () => void;
|
|
23
29
|
[navigationDeadlineKey]?: number;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
@@ -17,6 +17,11 @@ export const extendTimeoutKey = Symbol('extendTimeout');
|
|
|
17
17
|
* bail, so a request whose timeout already elapsed does not carry on after the crawler moved past it.
|
|
18
18
|
*/
|
|
19
19
|
export const timeoutExpiredKey = Symbol('timeoutExpired');
|
|
20
|
+
/**
|
|
21
|
+
* Stops the internal request timeout's clock until the returned callback is called. Error handling has a timeout
|
|
22
|
+
* of its own, and must not be cut short (and handled again) by the request timeout.
|
|
23
|
+
*/
|
|
24
|
+
export const suspendTimeoutKey = Symbol('suspendTimeout');
|
|
20
25
|
/**
|
|
21
26
|
* Milliseconds left in the shared navigation window for `ctx`, lazily starting the window on first use.
|
|
22
27
|
* @internal
|
|
@@ -46,21 +51,41 @@ export async function raceWithTimeout(context, work, { timeoutMillis, requestId
|
|
|
46
51
|
let deadline = Date.now() + timeoutMillis;
|
|
47
52
|
let settled = false;
|
|
48
53
|
let firedByTimeout = false;
|
|
54
|
+
let suspendedAt;
|
|
49
55
|
const timeout = new Promise((_, reject) => {
|
|
50
56
|
const fire = () => {
|
|
51
57
|
settled = true;
|
|
52
58
|
firedByTimeout = true;
|
|
53
59
|
reject(new TimeoutError(`Request timed out after ${timeoutMillis / 1e3} seconds (${requestId}).`));
|
|
54
60
|
};
|
|
61
|
+
const schedule = () => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
timer = setTimeout(fire, Math.max(deadline - Date.now(), 0));
|
|
64
|
+
};
|
|
55
65
|
timer = setTimeout(fire, timeoutMillis);
|
|
56
66
|
context[timeoutExpiredKey] = () => firedByTimeout;
|
|
57
67
|
context[extendTimeoutKey] = (extraMillis) => {
|
|
58
68
|
if (settled) {
|
|
59
69
|
return;
|
|
60
70
|
}
|
|
61
|
-
clearTimeout(timer);
|
|
62
71
|
deadline += extraMillis;
|
|
63
|
-
|
|
72
|
+
if (suspendedAt === undefined) {
|
|
73
|
+
schedule();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
context[suspendTimeoutKey] = () => {
|
|
77
|
+
if (settled || suspendedAt !== undefined) {
|
|
78
|
+
return () => { };
|
|
79
|
+
}
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
suspendedAt = Date.now();
|
|
82
|
+
return () => {
|
|
83
|
+
deadline += Date.now() - suspendedAt;
|
|
84
|
+
suspendedAt = undefined;
|
|
85
|
+
if (!settled) {
|
|
86
|
+
schedule();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
64
89
|
};
|
|
65
90
|
});
|
|
66
91
|
try {
|
|
@@ -149,7 +149,8 @@ export declare class Session implements ISession {
|
|
|
149
149
|
*/
|
|
150
150
|
setCookie(rawCookie: string, url: string): Promise<void>;
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
152
|
+
* Makes an unusable session's state terminal. Unlike `retire()`, this leaves the error score alone so that
|
|
153
|
+
* `isBlocked()` keeps telling a burned identity from one that merely ran out of uses.
|
|
153
154
|
*/
|
|
154
155
|
private maybeSelfRetire;
|
|
155
156
|
}
|
|
@@ -140,6 +140,8 @@ export class Session {
|
|
|
140
140
|
* It increases `usageCount` and potentially lowers the `errorScore` by the `errorScoreDecrement`.
|
|
141
141
|
*/
|
|
142
142
|
markGood() {
|
|
143
|
+
if (this.#retired)
|
|
144
|
+
return;
|
|
143
145
|
this.#usageCount += 1;
|
|
144
146
|
if (this.#errorScore > 0) {
|
|
145
147
|
this.#errorScore -= this.#errorScoreDecrement;
|
|
@@ -176,7 +178,7 @@ export class Session {
|
|
|
176
178
|
* For transient external failures (such as `5XX` responses), use `markBad()` instead.
|
|
177
179
|
*/
|
|
178
180
|
retire() {
|
|
179
|
-
if (this.#retired)
|
|
181
|
+
if (this.#retired && this.isBlocked())
|
|
180
182
|
return;
|
|
181
183
|
this.#errorScore += this.#maxErrorScore;
|
|
182
184
|
this.#usageCount += 1;
|
|
@@ -212,11 +214,12 @@ export class Session {
|
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
/**
|
|
215
|
-
*
|
|
217
|
+
* Makes an unusable session's state terminal. Unlike `retire()`, this leaves the error score alone so that
|
|
218
|
+
* `isBlocked()` keeps telling a burned identity from one that merely ran out of uses.
|
|
216
219
|
*/
|
|
217
220
|
maybeSelfRetire() {
|
|
218
221
|
if (!this.isUsable()) {
|
|
219
|
-
this
|
|
222
|
+
this.#retired = true;
|
|
220
223
|
}
|
|
221
224
|
}
|
|
222
225
|
}
|
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.208",
|
|
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": "^3.0.1",
|
|
43
43
|
"@apify/timeout": "^1.0.1",
|
|
44
44
|
"@apify/utilities": "^3.0.1",
|
|
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.208",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.208",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.208",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.208",
|
|
49
49
|
"@sapphire/async-queue": "^1.5.5",
|
|
50
50
|
"@standard-schema/spec": "^1.0.0",
|
|
51
51
|
"csv-stringify": "^6.5.2",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"zod": "^4.5.4"
|
|
58
58
|
},
|
|
59
59
|
"optionalDependencies": {
|
|
60
|
-
"@crawlee/impit-client": "4.0.0-beta.
|
|
60
|
+
"@crawlee/impit-client": "4.0.0-beta.208"
|
|
61
61
|
},
|
|
62
62
|
"lerna": {
|
|
63
63
|
"command": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "81d3fbc1abd10fd34be2694a98f4a36e2ebc2f64"
|
|
70
70
|
}
|