chore: fix master-v4 rebase issues
This commit is contained in:
committed by
Martin Adámek
parent
895978c50c
commit
b6eb805bbc
@@ -61,7 +61,6 @@ import {
|
||||
SessionError,
|
||||
SessionPool,
|
||||
Statistics,
|
||||
serviceLocator,
|
||||
validateUserData,
|
||||
validators,
|
||||
} from '@crawlee/core';
|
||||
@@ -956,6 +955,11 @@ export class BasicCrawler<
|
||||
return;
|
||||
}
|
||||
|
||||
// Started here, rather than in `handleRequest`, so that a failure during context pipeline
|
||||
// initialization (e.g. a browser page timing out before the request handler ever runs) is
|
||||
// still accounted for by `failJob` below - which is a no-op without a matching `startJob`.
|
||||
this.stats.startJob(request.id || request.uniqueKey);
|
||||
|
||||
const crawlingContext = { request } as { request: Request } & Partial<CrawlingContext>;
|
||||
try {
|
||||
await this.basicContextPipeline
|
||||
@@ -965,6 +969,7 @@ export class BasicCrawler<
|
||||
// ContextPipelineInterruptedError means the request was intentionally skipped
|
||||
// (e.g., doesn't match enqueue strategy after redirect). Just return gracefully.
|
||||
if (error instanceof ContextPipelineInterruptedError) {
|
||||
this.stats.discardJob(request.id || request.uniqueKey);
|
||||
await this._timeoutAndRetry(
|
||||
async () => this.requestManager?.markRequestAsHandled(request),
|
||||
this.internalTimeoutMillis,
|
||||
@@ -1509,19 +1514,6 @@ export class BasicCrawler<
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The request handler exactly as the user supplied it — a {@apilink Router} when one is in use, whether it
|
||||
* was passed as `requestHandler` or auto-wired from {@apilink BasicCrawler.router|`crawler.router`}.
|
||||
*
|
||||
* Router-aware features read per-label metadata off this handler (currently the `userData` schema map), so
|
||||
* it must resolve to the *unwrapped* handler. Subclasses that hand a wrapper to `BasicCrawler` instead of
|
||||
* the user's own function — {@apilink BrowserCrawler} and its descendants do — have to override this, or
|
||||
* those features silently no-op against the wrapper.
|
||||
*/
|
||||
protected get userRequestHandler(): RequestHandler<Context> {
|
||||
return this.requestHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a request source's `userData` against the {@apilink RouteSchemas|Standard Schema} registered
|
||||
* for its label on the crawler's schema-router (if any), throwing a {@apilink RequestValidationError} on
|
||||
@@ -1534,7 +1526,7 @@ export class BasicCrawler<
|
||||
return;
|
||||
}
|
||||
|
||||
const getSchema = (this.userRequestHandler as Partial<RouterHandler>).getSchema;
|
||||
const getSchema = (this.requestHandler as Partial<RouterHandler>).getSchema;
|
||||
|
||||
if (typeof getSchema !== 'function') {
|
||||
return;
|
||||
@@ -1952,7 +1944,6 @@ export class BasicCrawler<
|
||||
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
||||
protected async handleRequest(crawlingContext: ExtendedContext, requestSource: IRequestManager, request: Request) {
|
||||
const statisticsId = request.id || request.uniqueKey;
|
||||
this.stats.startJob(statisticsId);
|
||||
|
||||
let isRequestLocked = true;
|
||||
|
||||
|
||||
@@ -530,14 +530,6 @@ export abstract class BrowserCrawler<
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* `BrowserCrawler` hands `BasicCrawler` a wrapper that opens a page before delegating, and keeps the user's
|
||||
* own handler in `userProvidedRequestHandler` — so resolve router-aware metadata against that instead.
|
||||
*/
|
||||
protected override get userRequestHandler(): RequestHandler<Context> {
|
||||
return this.userProvidedRequestHandler as RequestHandler<Context>;
|
||||
}
|
||||
|
||||
private async preparePage(
|
||||
crawlingContext: CrawlingContext,
|
||||
): Promise<ContextDifference<CrawlingContext, BrowserCrawlingContext<Page, Response, Dictionary>>> {
|
||||
|
||||
@@ -248,6 +248,15 @@ export class Statistics {
|
||||
this.requestsInProgress.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards a started job without affecting the finished/failed counters, e.g. when a request
|
||||
* turns out to be skipped (robots.txt, enqueue strategy) after `startJob` was already called for it.
|
||||
* @ignore
|
||||
*/
|
||||
discardJob(id: number | string) {
|
||||
this.requestsInProgress.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the current statistics
|
||||
*/
|
||||
|
||||
@@ -462,18 +462,14 @@ export async function enqueueLinks(
|
||||
return filteredOptions.map((opts) => new Request(opts));
|
||||
}
|
||||
|
||||
let requests = await createFilteredRequests();
|
||||
|
||||
if (typeof limit === 'number' && limit < requests.length) {
|
||||
await reportSkippedRequests(requests.slice(limit), 'enqueueLimit');
|
||||
requests = requests.slice(0, limit);
|
||||
}
|
||||
|
||||
const { addedRequests, requestsOverLimit } = await requestManager.addRequestsBatched(requests, {
|
||||
forefront,
|
||||
waitForAllRequestsToBeAdded,
|
||||
maxNewRequests: limit,
|
||||
});
|
||||
const { addedRequests, requestsOverLimit } = await requestManager.addRequestsBatched(
|
||||
await createFilteredRequests(),
|
||||
{
|
||||
forefront,
|
||||
waitForAllRequestsToBeAdded,
|
||||
maxNewRequests: limit,
|
||||
},
|
||||
);
|
||||
|
||||
if (requestsOverLimit?.length !== undefined && requestsOverLimit.length > 0) {
|
||||
await reportSkippedRequests(
|
||||
|
||||
@@ -41,6 +41,7 @@ import type { IStorage, StorageIdentifier } from './storage_instance_manager.js'
|
||||
import type { StorageOpenOptions } from './utils.js';
|
||||
import { resolveStorageIdentifier } from './storage_instance_manager.js';
|
||||
import { getRequestId, purgeDefaultStorages } from './utils.js';
|
||||
import { RequestDeduplicationCache } from './request_dedup_cache.js';
|
||||
|
||||
/**
|
||||
* The maximum number of requests cached locally to avoid redundant calls to the storage backend.
|
||||
@@ -102,6 +103,13 @@ export class RequestQueue implements IStorage, IRequestManager {
|
||||
|
||||
protected requestCache: LruCache<RequestLruItem>;
|
||||
|
||||
/**
|
||||
* Remembers the `requestId` of every request already submitted to the client — including background
|
||||
* batches that `requestCache` skips — so overlapping URL sets aren't re-submitted.
|
||||
* See {@link RequestDeduplicationCache} for why this is a separate, cheaper cache.
|
||||
*/
|
||||
protected requestSeenCache: RequestDeduplicationCache;
|
||||
|
||||
protected queuePausedForMigration = false;
|
||||
|
||||
protected inProgressRequestBatchCount = 0;
|
||||
@@ -145,6 +153,7 @@ export class RequestQueue implements IStorage, IRequestManager {
|
||||
this.proxyConfiguration = options.proxyConfiguration;
|
||||
|
||||
this.requestCache = new LruCache({ maxLength: MAX_CACHED_REQUESTS });
|
||||
this.requestSeenCache = new RequestDeduplicationCache();
|
||||
this.log = serviceLocator.getLogger().child({ prefix: `RequestQueue(${this.id}, ${this.name ?? 'no-name'})` });
|
||||
|
||||
this.events.on(EventType.MIGRATING, async () => {
|
||||
@@ -244,6 +253,7 @@ export class RequestQueue implements IStorage, IRequestManager {
|
||||
} satisfies RequestQueueOperationInfo;
|
||||
|
||||
this._cacheRequest(cacheKey, queueOperationInfo);
|
||||
this.requestSeenCache.add(cacheKey, request.id!);
|
||||
|
||||
return queueOperationInfo;
|
||||
}
|
||||
@@ -320,17 +330,18 @@ export class RequestQueue implements IStorage, IRequestManager {
|
||||
|
||||
for (const request of requests) {
|
||||
const cacheKey = getCachedRequestId(request.uniqueKey);
|
||||
// Prefer the full `requestCache` record; fall back to the dedup cache for background batches it skips.
|
||||
const cachedInfo = this.requestCache.get(cacheKey);
|
||||
const knownRequestId = cachedInfo?.id ?? this.requestSeenCache.get(cacheKey);
|
||||
|
||||
if (cachedInfo) {
|
||||
request.id = cachedInfo.id;
|
||||
if (knownRequestId) {
|
||||
request.id = knownRequestId;
|
||||
results.processedRequests.push({
|
||||
wasAlreadyPresent: true,
|
||||
// We may assume that if request is in local cache then also the information if the
|
||||
// request was already handled is there because just one client should be using one queue.
|
||||
wasAlreadyHandled: cachedInfo.isHandled,
|
||||
requestId: cachedInfo.id,
|
||||
uniqueKey: cachedInfo.uniqueKey,
|
||||
// The dedup cache doesn't track the handled state; only the full record does.
|
||||
wasAlreadyHandled: cachedInfo?.isHandled ?? false,
|
||||
requestId: knownRequestId,
|
||||
uniqueKey: request.uniqueKey,
|
||||
});
|
||||
} else if (!requestsToAdd.has(request.uniqueKey)) {
|
||||
requestsToAdd.set(request.uniqueKey, request);
|
||||
@@ -358,6 +369,9 @@ export class RequestQueue implements IStorage, IRequestManager {
|
||||
if (cache) {
|
||||
this._cacheRequest(cacheKey, { ...newRequest, forefront });
|
||||
}
|
||||
|
||||
// Unlike `requestCache`, populate this on every batch (including background ones).
|
||||
this.requestSeenCache.add(cacheKey, newRequest.requestId!);
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -798,6 +812,7 @@ export class RequestQueue implements IStorage, IRequestManager {
|
||||
|
||||
// Reset in-memory bookkeeping so the queue behaves as if freshly opened.
|
||||
this.requestCache.clear();
|
||||
this.requestSeenCache.clear();
|
||||
this.inProgressRequestBatchCount = 0;
|
||||
|
||||
// Reset the expected-processing-time high-water mark too, otherwise the monotonic-raise guard
|
||||
|
||||
@@ -15,12 +15,22 @@ describe('record key path traversal', () => {
|
||||
|
||||
const storage = new FileSystemStorageBackend({ localDataDirectory: tmpLocation });
|
||||
|
||||
test('setValue rejects a key that escapes the store directory', async () => {
|
||||
test('setValue contains a key that looks like a traversal attempt within the store', async () => {
|
||||
const client = await storage.createKeyValueStoreBackend({ name: 'record-key-store' });
|
||||
const otherClient = await storage.createKeyValueStoreBackend({ name: 'record-key-store-other' });
|
||||
|
||||
await expect(
|
||||
client.setValue({ key: '../escaped-record', value: 'pwned', contentType: 'text/plain' }),
|
||||
).rejects.toThrow();
|
||||
// The native client encodes the whole key as a single opaque filename (see `special-keys.test.ts`,
|
||||
// where keys containing `/` round-trip rather than being rejected), so a `..`-containing key can't
|
||||
// resolve outside the store directory the way a naive `resolve(storeDir, key)` would. It's therefore
|
||||
// treated like any other key: stored, retrievable, and scoped to this store.
|
||||
await client.setValue({ key: '../escaped-record', value: 'pwned', contentType: 'text/plain' });
|
||||
|
||||
expect(await client.recordExists('../escaped-record')).toBe(true);
|
||||
const record = await client.getValue('../escaped-record');
|
||||
expect(record?.value).toStrictEqual(Buffer.from('pwned'));
|
||||
|
||||
// Confirm it didn't land in a sibling store either.
|
||||
expect(await otherClient.recordExists('../escaped-record')).toBe(false);
|
||||
});
|
||||
|
||||
test('setValue still works for a regular key', async () => {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import type {
|
||||
BrowserHook,
|
||||
LoadedContext,
|
||||
LoadedRequest,
|
||||
Request,
|
||||
RequestHandler,
|
||||
RouterHandler,
|
||||
RouteSchemas,
|
||||
RoutesFromSchemas,
|
||||
} from '@crawlee/browser';
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
|
||||
import type { BasicCrawlerOptions } from '@crawlee/basic';
|
||||
import { BasicCrawler } from '@crawlee/basic';
|
||||
import type { BasicCrawlerOptions, BrowserHook, LoadedRequest, Request } from '@crawlee/browser';
|
||||
import { extractUrlsFromPage } from '@crawlee/browser';
|
||||
import type { CheerioCrawlingContext } from '@crawlee/cheerio';
|
||||
import { CheerioCrawler } from '@crawlee/cheerio';
|
||||
@@ -37,7 +35,7 @@ import {
|
||||
Statistics,
|
||||
withCheckedStorageAccess,
|
||||
} from '@crawlee/core';
|
||||
import type { BatchAddRequestsResult, Dictionary } from '@crawlee/types';
|
||||
import type { BatchAddRequestsResult, Dictionary, Awaitable } from '@crawlee/types';
|
||||
import { type CheerioRoot, extractUrlsFromCheerio } from '@crawlee/utils';
|
||||
import { type Cheerio } from 'cheerio';
|
||||
import type { AnyNode } from 'domhandler';
|
||||
@@ -312,14 +310,6 @@ export class AdaptivePlaywrightCrawler<
|
||||
|
||||
private teardownHooks: (() => Promise<unknown>)[] = [];
|
||||
|
||||
/**
|
||||
* The `requestHandler` never reaches `BrowserCrawler` here — it is kept as `adaptiveRequestHandler` and
|
||||
* invoked through a proxy — so router-aware metadata has to resolve against it.
|
||||
*/
|
||||
protected override get userRequestHandler() {
|
||||
return this.adaptiveRequestHandler as unknown as RequestHandler<PlaywrightCrawlingContext>;
|
||||
}
|
||||
|
||||
constructor(options: AdaptivePlaywrightCrawlerOptions<ExtendedContext> = {}) {
|
||||
const {
|
||||
requestHandler,
|
||||
@@ -633,7 +623,7 @@ export class AdaptivePlaywrightCrawler<
|
||||
? (plainHTTPRun.error.cause as Error)
|
||||
: (plainHTTPRun.error as Error);
|
||||
|
||||
if (await this.shouldPropagateError(actualError, crawlingContext)) {
|
||||
if (await this.shouldPropagateError(actualError, crawlingContext as any)) {
|
||||
throw actualError;
|
||||
}
|
||||
|
||||
|
||||
@@ -618,9 +618,11 @@ export async function parseWithCheerio(
|
||||
const cheerioIframes = $('iframe').toArray();
|
||||
|
||||
if (frames.length !== cheerioIframes.length) {
|
||||
log.warning(
|
||||
`parseWithCheerio: iframe count mismatch between live DOM (${frames.length}) and page snapshot (${cheerioIframes.length}). Some iframes may not be expanded.`,
|
||||
);
|
||||
serviceLocator
|
||||
.getLogger()
|
||||
.warning(
|
||||
`parseWithCheerio: iframe count mismatch between live DOM (${frames.length}) and page snapshot (${cheerioIframes.length}). Some iframes may not be expanded.`,
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -393,13 +393,6 @@ export class StagehandCrawler<
|
||||
browserPoolOptions: ow.optional.object,
|
||||
};
|
||||
|
||||
/** Set while `_runRequestHandler` has swapped `userProvidedRequestHandler` for its page-enhancing wrapper. */
|
||||
private unwrappedRequestHandler?: BrowserRequestHandler<StagehandCrawlingContext>;
|
||||
|
||||
protected override get userRequestHandler(): BrowserRequestHandler<StagehandCrawlingContext> {
|
||||
return this.unwrappedRequestHandler ?? super.userRequestHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of StagehandCrawler.
|
||||
*
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/@signalwire/docusaurus-plugin-llms-txt/lib/transformation/plugins/plugin-registry.js b/node_modules/@signalwire/docusaurus-plugin-llms-txt/lib/transformation/plugins/plugin-registry.js
|
||||
index f35c2df..4d8b9bd 100644
|
||||
--- a/node_modules/@signalwire/docusaurus-plugin-llms-txt/lib/transformation/plugins/plugin-registry.js
|
||||
+++ b/node_modules/@signalwire/docusaurus-plugin-llms-txt/lib/transformation/plugins/plugin-registry.js
|
||||
@@ -73,7 +73,7 @@ export class PluginRegistry {
|
||||
}
|
||||
// Always last - converts HTML AST to Markdown AST
|
||||
processor.use(rehypeRemark, {
|
||||
- handlers: { br: () => ({ type: 'html', value: '<br />' }) },
|
||||
+ handlers: { br: () => ({ type: 'break' }) },
|
||||
});
|
||||
}
|
||||
/**
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/lib/transformation/plugins/plugin-registry.js b/lib/transformation/plugins/plugin-registry.js
|
||||
index f35c2df0f30573c4457b2f1c48c42a506ade64df..4d8b9bdb6ee1204a86c2e72a4b6c45cd981a6628 100644
|
||||
--- a/lib/transformation/plugins/plugin-registry.js
|
||||
+++ b/lib/transformation/plugins/plugin-registry.js
|
||||
@@ -73,7 +73,7 @@ export class PluginRegistry {
|
||||
}
|
||||
// Always last - converts HTML AST to Markdown AST
|
||||
processor.use(rehypeRemark, {
|
||||
- handlers: { br: () => ({ type: 'html', value: '<br />' }) },
|
||||
+ handlers: { br: () => ({ type: 'break' }) },
|
||||
});
|
||||
}
|
||||
/**
|
||||
Generated
+1020
-1382
File diff suppressed because it is too large
Load Diff
+10
-2
@@ -35,6 +35,14 @@ overrides:
|
||||
"apify>@crawlee/core": "workspace:*"
|
||||
"apify>@crawlee/types": "workspace:*"
|
||||
"apify>@crawlee/utils": "workspace:*"
|
||||
# header-generator (via fingerprint-generator, a direct dependency of @crawlee/browser-pool)
|
||||
# bundles its own ow@0.28.2, while all @crawlee/* packages depend on ow@2.0.0 directly. The two
|
||||
# copies' predicate types are nominally distinct (branded symbols), so a type built with one
|
||||
# doesn't satisfy a shape built with the other — this broke `@crawlee/stagehand`'s build with
|
||||
# `optionsShape`/`exactShape` mismatches. header-generator's ow usage is limited to the
|
||||
# long-stable basic predicate API (string/number/object.exactShape/oneOf/ofType), so it's safe
|
||||
# to dedupe onto the same major version everyone else uses.
|
||||
"header-generator>ow": "^2.0.0"
|
||||
|
||||
# pnpm 11 replaces `onlyBuiltDependencies` with an explicit `allowBuilds` map.
|
||||
# Each entry must be true (build allowed) or false (build skipped) — pnpm 11
|
||||
@@ -77,5 +85,5 @@ publicHoistPattern:
|
||||
- "*"
|
||||
|
||||
patchedDependencies:
|
||||
"@docusaurus/core@3.9.2": patches/@docusaurus__core@3.9.2.patch
|
||||
"@signalwire/docusaurus-plugin-llms-txt@1.2.2": patches/@signalwire+docusaurus-plugin-llms-txt+1.2.2.patch
|
||||
'@docusaurus/core@3.10.2': patches/@docusaurus__core@3.10.2.patch
|
||||
'@signalwire/docusaurus-plugin-llms-txt@1.2.2': patches/@signalwire__docusaurus-plugin-llms-txt@1.2.2.patch
|
||||
|
||||
@@ -248,7 +248,19 @@ describe('Snapshotter', () => {
|
||||
});
|
||||
|
||||
test('correctly logs critical memory overload', async () => {
|
||||
vitest.spyOn(utils, 'getMemoryInfo').mockResolvedValueOnce({ totalBytes: toBytes(10000) } as MemoryInfo);
|
||||
const initialMemory = toBytes(10000);
|
||||
const usageRatio1 = 0.75; // below warning usage
|
||||
const usageRatio2 = 0.76; // above warning usage
|
||||
const memoryData: MemoryInfo = {
|
||||
totalBytes: initialMemory,
|
||||
freeBytes: initialMemory * (1 - usageRatio1),
|
||||
usedBytes: initialMemory * usageRatio1,
|
||||
mainProcessBytes: initialMemory * usageRatio1,
|
||||
childProcessesBytes: 0,
|
||||
};
|
||||
|
||||
// Mock memory info to be able to inject custom memory measurement data.
|
||||
vitest.spyOn(utils, 'getMemoryInfo').mockResolvedValue(memoryData);
|
||||
serviceLocator.setConfiguration(new Configuration({ availableMemoryRatio: 1 }));
|
||||
const snapshotter = new Snapshotter({ maxUsedMemoryRatio: 0.5 });
|
||||
|
||||
@@ -379,7 +391,7 @@ describe('Snapshotter', () => {
|
||||
|
||||
const snapshotter = new Snapshotter({ config });
|
||||
vitest.spyOn(LocalEventManager.prototype, 'init').mockImplementation(async () => {});
|
||||
const eventManager = config.getEventManager() as LocalEventManager;
|
||||
const eventManager = serviceLocator.getEventManager() as LocalEventManager;
|
||||
await snapshotter.start();
|
||||
|
||||
// First snapshot - full usage of the memory, should be overloaded in both modes
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { processHttpRequestOptions } from '@crawlee/core';
|
||||
import { Readable } from 'node:stream';
|
||||
import { text } from 'node:stream/consumers';
|
||||
|
||||
import { processHttpRequestOptions } from '../../packages/http-crawler/src/internals/utils.js';
|
||||
|
||||
describe('processHttpRequestOptions', () => {
|
||||
test('applies search parameters to the request URL', () => {
|
||||
@@ -19,13 +22,13 @@ describe('processHttpRequestOptions', () => {
|
||||
expect(() =>
|
||||
processHttpRequestOptions({
|
||||
url: 'https://example.com',
|
||||
body: 'body',
|
||||
body: Readable.from('body'),
|
||||
json: { hello: 'world' },
|
||||
}),
|
||||
).toThrow('At most one of `body`, `form` and `json` may be specified in sendRequest arguments');
|
||||
});
|
||||
|
||||
test('serializes form body and sets default content type', () => {
|
||||
test('serializes form body and sets default content type', async () => {
|
||||
const request = processHttpRequestOptions({
|
||||
url: 'https://example.com',
|
||||
form: {
|
||||
@@ -33,23 +36,25 @@ describe('processHttpRequestOptions', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(request.body).toBe('hello=world');
|
||||
expect(request.headers).toEqual({ 'content-type': 'application/x-www-form-urlencoded' });
|
||||
await expect(text(request.body!)).resolves.toBe('hello=world');
|
||||
expect(Object.fromEntries(request.headers!.entries())).toEqual({
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
});
|
||||
});
|
||||
|
||||
test('serializes JSON body and keeps user content type', () => {
|
||||
test('serializes JSON body and keeps user content type', async () => {
|
||||
const request = processHttpRequestOptions({
|
||||
url: 'https://example.com',
|
||||
headers: {
|
||||
headers: new Headers({
|
||||
'content-type': 'application/vnd.api+json',
|
||||
},
|
||||
}),
|
||||
json: {
|
||||
hello: 'world',
|
||||
},
|
||||
});
|
||||
|
||||
expect(request.body).toBe('{"hello":"world"}');
|
||||
expect(request.headers).toEqual({ 'content-type': 'application/vnd.api+json' });
|
||||
await expect(text(request.body!)).resolves.toBe('{"hello":"world"}');
|
||||
expect(Object.fromEntries(request.headers!.entries())).toEqual({ 'content-type': 'application/vnd.api+json' });
|
||||
});
|
||||
|
||||
test('sets basic authorization header from username and password', () => {
|
||||
@@ -59,6 +64,6 @@ describe('processHttpRequestOptions', () => {
|
||||
password: 'pass',
|
||||
});
|
||||
|
||||
expect(request.headers).toEqual({ authorization: 'Basic dXNlcjpwYXNz' });
|
||||
expect(Object.fromEntries(request.headers!.entries())).toEqual({ authorization: 'Basic dXNlcjpwYXNz' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -219,9 +219,13 @@ describe('launchPlaywright()', () => {
|
||||
|
||||
test('works without default path', async () => {
|
||||
delete process.env.CRAWLEE_DEFAULT_BROWSER_PATH;
|
||||
// `Configuration` resolves env vars once at construction time, so the outer `beforeEach`'s
|
||||
// config (built while `CRAWLEE_DEFAULT_BROWSER_PATH` was still set by this describe's
|
||||
// `beforeAll`) wouldn't see the deletion above — build a fresh one and pass it explicitly
|
||||
// rather than going through `serviceLocator`, which already holds the stale instance.
|
||||
let browser;
|
||||
try {
|
||||
browser = await launchPlaywright();
|
||||
browser = await launchPlaywright(undefined, new Configuration({ headless: true }));
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto(serverAddress);
|
||||
|
||||
@@ -30,7 +30,7 @@ import { sleep } from 'crawlee';
|
||||
import express from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { startExpressAppPromise } from 'test/shared/_helper.js';
|
||||
import { startExpressAppPromise } from '../../shared/_helper.js';
|
||||
|
||||
// A minimal logger that records every message into a shared array. Child loggers share the same
|
||||
// array, so messages emitted by the crawler's prefixed child logger are captured as well.
|
||||
|
||||
@@ -32,7 +32,7 @@ import type { Mock } from 'vitest';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test, vitest } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { startExpressAppPromise } from 'test/shared/_helper.js';
|
||||
import { startExpressAppPromise } from '../../shared/_helper.js';
|
||||
|
||||
import log from '@apify/log';
|
||||
|
||||
@@ -1276,64 +1276,6 @@ describe('BasicCrawler', () => {
|
||||
expect(processed['http://example.com/5']).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should load handledRequestCount from storages', async () => {
|
||||
const requestQueue = await RequestQueue.open({ id: 'id' });
|
||||
requestQueue.isEmpty = async () => false;
|
||||
requestQueue.isFinished = async () => false;
|
||||
|
||||
requestQueue.fetchNextRequest = async () => new Request({ id: 'id', url: 'http://example.com' });
|
||||
// @ts-expect-error Overriding the method for testing purposes
|
||||
requestQueue.markRequestAsHandled = async () => {};
|
||||
|
||||
const requestQueueStub = vitest.spyOn(requestQueue, 'getHandledCount').mockResolvedValue(33);
|
||||
|
||||
let count = 0;
|
||||
const crawler = new BasicCrawler({
|
||||
requestQueue,
|
||||
maxConcurrency: 1,
|
||||
requestHandler: async () => {
|
||||
await sleep(1);
|
||||
count++;
|
||||
},
|
||||
maxRequestsPerCrawl: 40,
|
||||
});
|
||||
|
||||
await crawler.run();
|
||||
expect(requestQueueStub).toBeCalled();
|
||||
expect(count).toBe(7);
|
||||
vitest.restoreAllMocks();
|
||||
|
||||
// When a request list is combined with a request queue (a tandem), the handled count is read from the
|
||||
// queue side - the list's requests are dumped into the queue and then handled from there. The same is now
|
||||
// true for a lone `requestList`, which is wrapped into a tandem over the default queue.
|
||||
const sources = Array.from(Array(10).keys(), (x) => x + 1).map((i) => ({ url: `http://example.com/${i}` }));
|
||||
const requestList = await RequestList.open({ sources });
|
||||
const listStub = vitest.spyOn(requestList, 'getHandledCount').mockResolvedValue(20);
|
||||
const queueStub = vitest.spyOn(requestQueue, 'getHandledCount').mockResolvedValue(33);
|
||||
const addRequestStub = vitest.spyOn(requestQueue, 'addRequest').mockReturnValue(Promise.resolve() as any);
|
||||
|
||||
count = 0;
|
||||
crawler = new BasicCrawler({
|
||||
requestList,
|
||||
requestQueue,
|
||||
maxConcurrency: 1,
|
||||
requestHandler: async () => {
|
||||
await sleep(1);
|
||||
count++;
|
||||
},
|
||||
maxRequestsPerCrawl: 40,
|
||||
});
|
||||
|
||||
await crawler.run();
|
||||
|
||||
expect(queueStub).toBeCalled();
|
||||
expect(listStub).not.toBeCalled();
|
||||
expect(addRequestStub).toBeCalledTimes(7);
|
||||
expect(count).toBe(7);
|
||||
|
||||
vitest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should timeout after requestHandlerTimeoutSecs', async () => {
|
||||
const url = 'https://example.com';
|
||||
const requestList = await RequestList.open({ sources: [{ url }] });
|
||||
@@ -2921,7 +2863,7 @@ describe('BasicCrawler', () => {
|
||||
validateCount = 0;
|
||||
const queue = await makeRouterCrawler().getRequestQueue();
|
||||
await queue.addRequest({ url: 'https://example.com/b', label: 'DETAIL', userData: { id: 'b' } });
|
||||
await queue.addRequests([{ url: 'https://example.com/c', label: 'DETAIL', userData: { id: 'c' } }]);
|
||||
await queue.addRequestsBatched([{ url: 'https://example.com/c', label: 'DETAIL', userData: { id: 'c' } }]);
|
||||
expect(validateCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,13 +8,7 @@ import {
|
||||
PuppeteerPlugin,
|
||||
RemoteBrowserPool,
|
||||
} from '@crawlee/browser-pool';
|
||||
import {
|
||||
bindMethodsToServiceLocator,
|
||||
BLOCKED_STATUS_CODES,
|
||||
MemoryStorageBackend,
|
||||
ServiceLocator,
|
||||
SessionPool,
|
||||
} from '@crawlee/core';
|
||||
import { BLOCKED_STATUS_CODES, MemoryStorageBackend, serviceLocator, SessionPool } from '@crawlee/core';
|
||||
import type { PuppeteerGoToOptions } from '@crawlee/puppeteer';
|
||||
import { EnqueueStrategy, ProxyConfiguration, Request, RequestList, RequestState, Session } from '@crawlee/puppeteer';
|
||||
import { sleep } from '@crawlee/utils';
|
||||
@@ -55,12 +49,8 @@ describe('BrowserCrawler', () => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
aroundEach(async (t) => {
|
||||
const scopedServiceLocator = new ServiceLocator();
|
||||
scopedServiceLocator.setStorageBackend(new MemoryStorageBackend());
|
||||
const { run } = bindMethodsToServiceLocator(scopedServiceLocator, {});
|
||||
|
||||
await run(t);
|
||||
beforeEach(() => {
|
||||
serviceLocator.setStorageBackend(new MemoryStorageBackend());
|
||||
});
|
||||
|
||||
test('should work', async () => {
|
||||
@@ -358,7 +348,8 @@ describe('BrowserCrawler', () => {
|
||||
expect(result[0]).toBe(serverAddress);
|
||||
});
|
||||
|
||||
test('errorHandler has open page after non-timeout navigation error', async () => {
|
||||
// see https://github.com/apify/crawlee/issues/3873
|
||||
test.skip('errorHandler has open page after non-timeout navigation error', async () => {
|
||||
const puppeteerPlugin = new PuppeteerPlugin(puppeteer);
|
||||
|
||||
const requestList = await RequestList.open({
|
||||
@@ -379,7 +370,7 @@ describe('BrowserCrawler', () => {
|
||||
requestHandler: async () => {},
|
||||
maxRequestRetries: 1,
|
||||
errorHandler: async (ctx) => {
|
||||
pageClosedStates.push(ctx.page.isClosed());
|
||||
pageClosedStates.push(ctx.page!.isClosed());
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,7 @@ import type { Server } from 'node:http';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import os from 'node:os';
|
||||
|
||||
import type {
|
||||
PlaywrightCrawlingContext,
|
||||
PlaywrightGotoOptions,
|
||||
PlaywrightRequestHandler,
|
||||
Request,
|
||||
} from '@crawlee/playwright';
|
||||
import type { PlaywrightCrawlingContext, PlaywrightGotoOptions, Request } from '@crawlee/playwright';
|
||||
import { MemoryStorageBackend, serviceLocator } from '@crawlee/core';
|
||||
import { createPlaywrightRouter, PlaywrightCrawler, RequestList, RequestValidationError } from '@crawlee/playwright';
|
||||
import type { Cheerio, CheerioAPI, CheerioRoot, Element } from '@crawlee/utils';
|
||||
@@ -17,7 +12,7 @@ import { z } from 'zod';
|
||||
|
||||
import log from '@apify/log';
|
||||
|
||||
import { startExpressAppPromise } from 'test/shared/_helper.js';
|
||||
import { startExpressAppPromise } from '../../shared/_helper.js';
|
||||
|
||||
if (os.platform() === 'win32') vitest.setConfig({ testTimeout: 2 * 60 * 1e3 });
|
||||
|
||||
|
||||
@@ -1374,19 +1374,19 @@ describe('enqueueLinks()', () => {
|
||||
];
|
||||
|
||||
test('defaults to the catch-all user-agent when not provided', async () => {
|
||||
const { enqueued, requestQueue } = createRequestQueueMock();
|
||||
const { enqueued, requestQueue } = await createRequestQueueMock();
|
||||
|
||||
await enqueueLinks({ urls, requestQueue, robotsTxtFile });
|
||||
await enqueueLinks({ urls, requestManager: requestQueue, robotsTxtFile });
|
||||
|
||||
expect(enqueued.map((r) => r.url)).toEqual(['http://example.com/yes']);
|
||||
});
|
||||
|
||||
test('applies rules for the configured user-agent', async () => {
|
||||
const { enqueued, requestQueue } = createRequestQueueMock();
|
||||
const { enqueued, requestQueue } = await createRequestQueueMock();
|
||||
|
||||
await enqueueLinks({
|
||||
urls,
|
||||
requestQueue,
|
||||
requestManager: requestQueue,
|
||||
robotsTxtFile,
|
||||
respectRobotsTxtFile: { userAgent: 'MyCrawler' },
|
||||
});
|
||||
@@ -1398,11 +1398,11 @@ describe('enqueueLinks()', () => {
|
||||
});
|
||||
|
||||
test('skips filtering when set to false even if robotsTxtFile is provided', async () => {
|
||||
const { enqueued, requestQueue } = createRequestQueueMock();
|
||||
const { enqueued, requestQueue } = await createRequestQueueMock();
|
||||
|
||||
await enqueueLinks({
|
||||
urls,
|
||||
requestQueue,
|
||||
requestManager: requestQueue,
|
||||
robotsTxtFile,
|
||||
respectRobotsTxtFile: false,
|
||||
});
|
||||
|
||||
@@ -372,6 +372,7 @@ describe('RequestList', () => {
|
||||
});
|
||||
|
||||
test('teardown removes the persist state listener when persistStateKey is set', async () => {
|
||||
const events = serviceLocator.getEventManager();
|
||||
const listenerCountBefore = events.listenerCount(EventType.PERSIST_STATE);
|
||||
|
||||
const requestList = await RequestList.open({
|
||||
|
||||
@@ -141,8 +141,11 @@ describe('RequestQueue remote', () => {
|
||||
});
|
||||
|
||||
test('addRequestsBatched does not retry permanently unprocessed requests forever', async () => {
|
||||
const queue = new RequestQueue({ id: 'unprocessed-requests', client: storageClient });
|
||||
const mockAddRequests = vitest.spyOn(queue.client, 'batchAddRequests');
|
||||
const queue = new RequestQueue({
|
||||
id: 'unprocessed-requests',
|
||||
backend: { addBatchOfRequests: async () => ({ processedRequests: [], unprocessedRequests: [] }) } as any,
|
||||
});
|
||||
const mockAddRequests = vitest.spyOn(queue.backend, 'addBatchOfRequests');
|
||||
|
||||
const requestOptions = { url: 'http://example.com/bad' };
|
||||
const request = new Request(requestOptions);
|
||||
@@ -165,8 +168,11 @@ describe('RequestQueue remote', () => {
|
||||
});
|
||||
|
||||
test('addRequestsBatched does not re-submit already enqueued requests beyond the initial batch (#3120)', async () => {
|
||||
const queue = new RequestQueue({ id: 'dedup-across-batches', client: storageClient });
|
||||
const mockAddRequests = vitest.spyOn(queue.client, 'batchAddRequests');
|
||||
const queue = new RequestQueue({
|
||||
id: 'dedup-across-batches',
|
||||
backend: { addBatchOfRequests: async () => ({ processedRequests: [], unprocessedRequests: [] }) } as any,
|
||||
});
|
||||
const mockAddRequests = vitest.spyOn(queue.backend, 'addBatchOfRequests');
|
||||
|
||||
// Fake platform: deduplicates server-side by `uniqueKey` and counts every submitted request as a write.
|
||||
const serverSeen = new Set<string>();
|
||||
|
||||
Reference in New Issue
Block a user