@crawlee/browser-pool 4.0.0-beta.8 → 4.0.0-beta.80

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.
Files changed (83) hide show
  1. package/README.md +17 -13
  2. package/abstract-classes/browser-controller.d.ts +67 -8
  3. package/abstract-classes/browser-controller.js +8 -11
  4. package/abstract-classes/browser-plugin.d.ts +38 -3
  5. package/abstract-classes/browser-plugin.js +65 -7
  6. package/anonymize-proxy.d.ts +5 -2
  7. package/anonymize-proxy.js +9 -5
  8. package/browser-pool.d.ts +88 -14
  9. package/browser-pool.js +151 -28
  10. package/container-proxy-server.d.ts +0 -1
  11. package/container-proxy-server.js +0 -1
  12. package/events.d.ts +0 -1
  13. package/events.js +0 -1
  14. package/fingerprinting/hooks.d.ts +0 -1
  15. package/fingerprinting/hooks.js +24 -9
  16. package/fingerprinting/types.d.ts +0 -1
  17. package/fingerprinting/types.js +0 -1
  18. package/fingerprinting/utils.d.ts +0 -1
  19. package/fingerprinting/utils.js +0 -1
  20. package/index.d.ts +14 -7
  21. package/index.js +5 -3
  22. package/launch-context.d.ts +19 -3
  23. package/launch-context.js +12 -4
  24. package/package.json +7 -8
  25. package/playwright/playwright-browser.d.ts +0 -1
  26. package/playwright/playwright-browser.js +0 -1
  27. package/playwright/playwright-controller.d.ts +0 -1
  28. package/playwright/playwright-controller.js +20 -10
  29. package/playwright/playwright-plugin.d.ts +6 -1
  30. package/playwright/playwright-plugin.js +27 -4
  31. package/proxy-server.d.ts +0 -1
  32. package/proxy-server.js +0 -1
  33. package/puppeteer/puppeteer-controller.d.ts +0 -1
  34. package/puppeteer/puppeteer-controller.js +16 -9
  35. package/puppeteer/puppeteer-plugin.d.ts +3 -1
  36. package/puppeteer/puppeteer-plugin.js +81 -44
  37. package/remote-browser-pool.d.ts +172 -0
  38. package/remote-browser-pool.js +191 -0
  39. package/remote-browser-provider.d.ts +83 -0
  40. package/remote-browser-provider.js +67 -0
  41. package/utils.d.ts +7 -1
  42. package/utils.js +19 -1
  43. package/abstract-classes/browser-controller.d.ts.map +0 -1
  44. package/abstract-classes/browser-controller.js.map +0 -1
  45. package/abstract-classes/browser-plugin.d.ts.map +0 -1
  46. package/abstract-classes/browser-plugin.js.map +0 -1
  47. package/anonymize-proxy.d.ts.map +0 -1
  48. package/anonymize-proxy.js.map +0 -1
  49. package/browser-pool.d.ts.map +0 -1
  50. package/browser-pool.js.map +0 -1
  51. package/container-proxy-server.d.ts.map +0 -1
  52. package/container-proxy-server.js.map +0 -1
  53. package/events.d.ts.map +0 -1
  54. package/events.js.map +0 -1
  55. package/fingerprinting/hooks.d.ts.map +0 -1
  56. package/fingerprinting/hooks.js.map +0 -1
  57. package/fingerprinting/types.d.ts.map +0 -1
  58. package/fingerprinting/types.js.map +0 -1
  59. package/fingerprinting/utils.d.ts.map +0 -1
  60. package/fingerprinting/utils.js.map +0 -1
  61. package/index.d.ts.map +0 -1
  62. package/index.js.map +0 -1
  63. package/launch-context.d.ts.map +0 -1
  64. package/launch-context.js.map +0 -1
  65. package/logger.d.ts +0 -3
  66. package/logger.d.ts.map +0 -1
  67. package/logger.js +0 -5
  68. package/logger.js.map +0 -1
  69. package/playwright/playwright-browser.d.ts.map +0 -1
  70. package/playwright/playwright-browser.js.map +0 -1
  71. package/playwright/playwright-controller.d.ts.map +0 -1
  72. package/playwright/playwright-controller.js.map +0 -1
  73. package/playwright/playwright-plugin.d.ts.map +0 -1
  74. package/playwright/playwright-plugin.js.map +0 -1
  75. package/proxy-server.d.ts.map +0 -1
  76. package/proxy-server.js.map +0 -1
  77. package/puppeteer/puppeteer-controller.d.ts.map +0 -1
  78. package/puppeteer/puppeteer-controller.js.map +0 -1
  79. package/puppeteer/puppeteer-plugin.d.ts.map +0 -1
  80. package/puppeteer/puppeteer-plugin.js.map +0 -1
  81. package/tsconfig.build.tsbuildinfo +0 -1
  82. package/utils.d.ts.map +0 -1
  83. package/utils.js.map +0 -1
package/browser-pool.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { AsyncResource } from 'node:async_hooks';
2
+ import { SessionError, serviceLocator } from '@crawlee/core';
1
3
  import { FingerprintGenerator } from 'fingerprint-generator';
2
4
  import { FingerprintInjector } from 'fingerprint-injector';
3
5
  import { nanoid } from 'nanoid';
@@ -7,7 +9,7 @@ import QuickLRU from 'quick-lru';
7
9
  import { TypedEmitter } from 'tiny-typed-emitter';
8
10
  import { addTimeoutToPromise, tryCancel } from '@apify/timeout';
9
11
  import { createFingerprintPreLaunchHook, createPostPageCreateHook, createPrePageCreateHook, } from './fingerprinting/hooks.js';
10
- import { log } from './logger.js';
12
+ const PAGE_CLOSE_TIMEOUT_MILLIS = 5000;
11
13
  const PAGE_CLOSE_KILL_TIMEOUT_MILLIS = 1000;
12
14
  const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000;
13
15
  /**
@@ -60,6 +62,7 @@ const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000;
60
62
  export class BrowserPool extends TypedEmitter {
61
63
  browserPlugins;
62
64
  maxOpenPagesPerBrowser;
65
+ maxOpenBrowsers;
63
66
  retireBrowserAfterPageCount;
64
67
  operationTimeoutMillis;
65
68
  closeInactiveBrowserAfterMillis;
@@ -74,6 +77,7 @@ export class BrowserPool extends TypedEmitter {
74
77
  pageCounter = 0;
75
78
  pages = new Map();
76
79
  pageIds = new WeakMap();
80
+ startingBrowserControllers = new Set();
77
81
  activeBrowserControllers = new Set();
78
82
  retiredBrowserControllers = new Set();
79
83
  pageToBrowserController = new WeakMap();
@@ -83,8 +87,10 @@ export class BrowserPool extends TypedEmitter {
83
87
  browserKillerInterval = setInterval(async () => this._closeInactiveRetiredBrowsers(), BROWSER_KILLER_INTERVAL_MILLIS);
84
88
  browserRetireInterval;
85
89
  limiter = pLimit(1);
90
+ log;
86
91
  constructor(options) {
87
92
  super();
93
+ this.log = serviceLocator.getLogger().child({ prefix: 'BrowserPool' });
88
94
  this.browserKillerInterval.unref();
89
95
  ow(options, ow.object.exactShape({
90
96
  browserPlugins: ow.array.minLength(1),
@@ -114,6 +120,7 @@ export class BrowserPool extends TypedEmitter {
114
120
  }
115
121
  this.browserPlugins = browserPlugins;
116
122
  this.maxOpenPagesPerBrowser = maxOpenPagesPerBrowser;
123
+ this.maxOpenBrowsers = Infinity;
117
124
  this.retireBrowserAfterPageCount = retireBrowserAfterPageCount;
118
125
  this.operationTimeoutMillis = operationTimeoutSecs * 1000;
119
126
  this.closeInactiveBrowserAfterMillis = closeInactiveBrowserAfterSecs * 1000;
@@ -142,23 +149,46 @@ export class BrowserPool extends TypedEmitter {
142
149
  * Opens a new page in one of the running browsers or launches
143
150
  * a new browser and opens a page there, if no browsers are active,
144
151
  * or their page limits have been exceeded.
152
+ *
153
+ * **Session injection (best-effort):** When a {@link NewPageOptions.session|session} is
154
+ * provided, this implementation uses it as a cache key for browser fingerprints (when
155
+ * fingerprinting is enabled) and reads
156
+ * {@link ProxyInfo.url|session.proxyInfo.url} /
157
+ * {@link ProxyInfo.ignoreTlsErrors|session.proxyInfo.ignoreTlsErrors} as defaults
158
+ * for `proxyUrl` and `ignoreTlsErrors` respectively. Explicit `proxyUrl` /
159
+ * `ignoreTlsErrors` values in the options take precedence.
160
+ *
161
+ * Beyond fingerprint caching and proxy configuration, no other session
162
+ * properties are consumed — cookie and header injection remain the
163
+ * crawler's responsibility.
145
164
  */
146
165
  async newPage(options = {}) {
147
- const { id = nanoid(), pageOptions, browserPlugin = this._pickBrowserPlugin(), proxyUrl, proxyTier } = options;
166
+ const { id = nanoid(), pageOptions, browserPlugin = this._pickBrowserPlugin(), session, proxyUrl = session?.proxyInfo?.url, ignoreTlsErrors = session?.proxyInfo?.ignoreTlsErrors, } = options;
148
167
  if (this.pages.has(id)) {
149
168
  throw new Error(`Page with ID: ${id} already exists.`);
150
169
  }
151
170
  if (browserPlugin && !this.browserPlugins.includes(browserPlugin)) {
152
171
  throw new Error('Provided browserPlugin is not one of the plugins used by BrowserPool.');
153
172
  }
173
+ // Bind the limiter callback to the current async-hooks context. p-limit
174
+ // otherwise resumes queued callbacks in the previous task's
175
+ // AsyncLocalStorage context, leaking aborted cancelTasks across unrelated
176
+ // requests (https://github.com/apify/crawlee/issues/3670). Mirrors the
177
+ // fix p-limit landed upstream in v5 (sindresorhus/p-limit#71); v5 is an
178
+ // ESM-only rewrite, so we can't bump it in Crawlee v3.
179
+ // TODO(crawlee@v4): bump p-limit to v5 and drop this AsyncResource.bind wrapper.
154
180
  // Limiter is necessary - https://github.com/apify/crawlee/issues/1126
155
- return this.limiter(async () => {
156
- let browserController = this._pickBrowserWithFreeCapacity(browserPlugin, { proxyTier, proxyUrl });
181
+ return this.limiter(AsyncResource.bind(async () => {
182
+ let browserController = this._pickBrowserWithFreeCapacity(browserPlugin, { proxyUrl });
157
183
  if (!browserController)
158
- browserController = await this._launchBrowser(id, { browserPlugin, proxyTier, proxyUrl });
184
+ browserController = await this._launchBrowser(id, {
185
+ browserPlugin,
186
+ proxyUrl,
187
+ ignoreTlsErrors,
188
+ });
159
189
  tryCancel();
160
- return await this._createPageForBrowser(id, browserController, pageOptions, proxyUrl);
161
- });
190
+ return await this._createPageForBrowser(id, browserController, pageOptions, proxyUrl, ignoreTlsErrors);
191
+ }));
162
192
  }
163
193
  /**
164
194
  * Unlike {@link newPage}, `newPageInNewBrowser` always launches a new
@@ -238,7 +268,7 @@ export class BrowserPool extends TypedEmitter {
238
268
  getPageId(page) {
239
269
  return this.pageIds.get(page);
240
270
  }
241
- async _createPageForBrowser(pageId, browserController, pageOptions = {}, proxyUrl) {
271
+ async _createPageForBrowser(pageId, browserController, pageOptions = {}, proxyUrl, ignoreTlsErrors) {
242
272
  // This is needed for concurrent newPage calls to wait for the browser launch.
243
273
  // It's not ideal though, we need to come up with a better API.
244
274
  // eslint-disable-next-line dot-notation -- accessing private property
@@ -247,6 +277,12 @@ export class BrowserPool extends TypedEmitter {
247
277
  const finalPageOptions = browserController.launchContext.useIncognitoPages ? pageOptions : undefined;
248
278
  if (finalPageOptions) {
249
279
  Object.assign(finalPageOptions, browserController.normalizeProxyOptions(proxyUrl, pageOptions));
280
+ if (ignoreTlsErrors) {
281
+ Object.assign(finalPageOptions, {
282
+ ignoreHTTPSErrors: true,
283
+ acceptInsecureCerts: true,
284
+ });
285
+ }
250
286
  }
251
287
  await this._executeHooks(this.prePageCreateHooks, pageId, browserController, finalPageOptions);
252
288
  tryCancel();
@@ -278,11 +314,14 @@ export class BrowserPool extends TypedEmitter {
278
314
  *
279
315
  */
280
316
  retireBrowserController(browserController) {
281
- const hasBeenRetiredOrKilled = !this.activeBrowserControllers.has(browserController);
317
+ const isStarting = this.startingBrowserControllers.has(browserController);
318
+ const isActive = this.activeBrowserControllers.has(browserController);
319
+ const hasBeenRetiredOrKilled = !isStarting && !isActive;
282
320
  if (hasBeenRetiredOrKilled)
283
321
  return;
284
322
  this.retiredBrowserControllers.add(browserController);
285
323
  this.emit("browserRetired" /* BROWSER_POOL_EVENTS.BROWSER_RETIRED */, browserController);
324
+ this.startingBrowserControllers.delete(browserController);
286
325
  this.activeBrowserControllers.delete(browserController);
287
326
  }
288
327
  /**
@@ -294,12 +333,65 @@ export class BrowserPool extends TypedEmitter {
294
333
  if (browserController)
295
334
  this.retireBrowserController(browserController);
296
335
  }
336
+ /**
337
+ * Releases a page back to the pool. The page is closed and, if the
338
+ * optional `error` is a {@link SessionError}, the browser controller
339
+ * that served the page is retired so that its tainted state (cookies,
340
+ * storage, etc.) cannot leak into future sessions.
341
+ *
342
+ * This is the primary way the crawler should return pages to the pool.
343
+ *
344
+ * @param page The page to release.
345
+ * @param options.error The error that caused the page to be released, if any.
346
+ */
347
+ async closePage(page, options) {
348
+ if (options?.error instanceof SessionError) {
349
+ this.retireBrowserByPage(page);
350
+ }
351
+ // Puppeteer 25+ can hang `page.close()` indefinitely when the page's navigation was aborted, don't let it block the crawler.
352
+ await addTimeoutToPromise(async () => page.close(), PAGE_CLOSE_TIMEOUT_MILLIS, `page.close() timed out after ${PAGE_CLOSE_TIMEOUT_MILLIS / 1000} seconds`);
353
+ }
354
+ /**
355
+ * Extracts the relevant state (currently just cookies) from a page via its
356
+ * owning {@link BrowserController}. Returns empty state when the page is
357
+ * no longer associated with a controller.
358
+ *
359
+ * As with {@link BrowserPool.injectPageState}, cookies are isolated per
360
+ * page only when the pool is configured with `useIncognitoPages: true`.
361
+ * With the default `useIncognitoPages: false`, the extracted cookies
362
+ * include those set by any sibling page sharing the same browser.
363
+ */
364
+ async extractPageState(page) {
365
+ const controller = this.getBrowserControllerByPage(page);
366
+ if (!controller) {
367
+ return { cookies: [] };
368
+ }
369
+ return { cookies: await controller.getCookies(page) };
370
+ }
371
+ /**
372
+ * Injects state into a page via its owning {@link BrowserController}.
373
+ *
374
+ * No-op when the page is no longer associated with a controller.
375
+ *
376
+ * Note that cookies are isolated per page only when the pool is configured
377
+ * with `useIncognitoPages: true` — each page then gets its own browser
378
+ * context. With the default `useIncognitoPages: false`, all pages in a
379
+ * browser share a single context, so injected cookies are visible to every
380
+ * page served by that browser.
381
+ */
382
+ async injectPageState(page, state) {
383
+ const controller = this.getBrowserControllerByPage(page);
384
+ if (!controller) {
385
+ return;
386
+ }
387
+ await controller.setCookies(page, state.cookies);
388
+ }
297
389
  /**
298
390
  * Removes all active browsers from the pool. The browsers will be
299
391
  * closed after all their pages are closed.
300
392
  */
301
393
  retireAllBrowsers() {
302
- this.activeBrowserControllers.forEach((controller) => {
394
+ [...this.startingBrowserControllers, ...this.activeBrowserControllers].forEach((controller) => {
303
395
  this.retireBrowserController(controller);
304
396
  });
305
397
  }
@@ -326,23 +418,36 @@ export class BrowserPool extends TypedEmitter {
326
418
  this._teardown();
327
419
  }
328
420
  _teardown() {
421
+ this.startingBrowserControllers.clear();
329
422
  this.activeBrowserControllers.clear();
330
423
  this.retiredBrowserControllers.clear();
331
424
  this.removeAllListeners();
332
425
  }
333
426
  _getAllBrowserControllers() {
334
- return new Set([...this.activeBrowserControllers, ...this.retiredBrowserControllers]);
427
+ return new Set([
428
+ ...this.startingBrowserControllers,
429
+ ...this.activeBrowserControllers,
430
+ ...this.retiredBrowserControllers,
431
+ ]);
335
432
  }
336
433
  async _launchBrowser(pageId, options) {
337
- const { browserPlugin, launchOptions, proxyTier, proxyUrl } = options;
434
+ const { browserPlugin, launchOptions, proxyUrl, ignoreTlsErrors } = options;
338
435
  const browserController = browserPlugin.createController();
339
- this.activeBrowserControllers.add(browserController);
436
+ this.startingBrowserControllers.add(browserController);
340
437
  const launchContext = browserPlugin.createLaunchContext({
341
438
  id: pageId,
342
439
  launchOptions,
343
- proxyTier,
344
440
  proxyUrl,
345
441
  });
442
+ // Disable SSL verification for MITM proxies
443
+ if (ignoreTlsErrors) {
444
+ /**
445
+ * @see https://playwright.dev/docs/api/class-browser/#browser-new-context
446
+ * @see https://github.com/puppeteer/puppeteer/blob/main/docs/api.md
447
+ */
448
+ launchContext.launchOptions.ignoreHTTPSErrors = true;
449
+ launchContext.launchOptions.acceptInsecureCerts = true;
450
+ }
346
451
  try {
347
452
  // If the hooks or the launch fails, we need to delete the controller,
348
453
  // because otherwise it would be stuck in limbo without a browser.
@@ -353,11 +458,10 @@ export class BrowserPool extends TypedEmitter {
353
458
  browserController.assignBrowser(browser, launchContext);
354
459
  }
355
460
  catch (err) {
356
- this.activeBrowserControllers.delete(browserController);
461
+ this.startingBrowserControllers.delete(browserController);
357
462
  throw err;
358
463
  }
359
- log.debug('Launched new browser.', { id: browserController.id });
360
- browserController.proxyTier = proxyTier;
464
+ this.log.debug('Launched new browser.', { id: browserController.id });
361
465
  browserController.proxyUrl = proxyUrl;
362
466
  try {
363
467
  // If the launch fails on the post-launch hooks, we need to clean up
@@ -365,9 +469,9 @@ export class BrowserPool extends TypedEmitter {
365
469
  await this._executeHooks(this.postLaunchHooks, pageId, browserController);
366
470
  }
367
471
  catch (err) {
368
- this.activeBrowserControllers.delete(browserController);
472
+ this.startingBrowserControllers.delete(browserController);
369
473
  browserController.close().catch((closeErr) => {
370
- log.error(`Could not close browser whose post-launch hooks failed.\nCause:${closeErr.message}`, {
474
+ this.log.error(`Could not close browser whose post-launch hooks failed.\nCause:${closeErr.message}`, {
371
475
  id: browserController.id,
372
476
  });
373
477
  });
@@ -375,6 +479,8 @@ export class BrowserPool extends TypedEmitter {
375
479
  }
376
480
  tryCancel();
377
481
  browserController.activate();
482
+ this.startingBrowserControllers.delete(browserController);
483
+ this.activeBrowserControllers.add(browserController);
378
484
  this.emit("browserLaunched" /* BROWSER_POOL_EVENTS.BROWSER_LAUNCHED */, browserController);
379
485
  return browserController;
380
486
  }
@@ -392,13 +498,11 @@ export class BrowserPool extends TypedEmitter {
392
498
  const hasCapacity = controller.activePages < this.maxOpenPagesPerBrowser;
393
499
  const isCorrectPlugin = controller.browserPlugin === browserPlugin;
394
500
  const isSameProxyUrl = controller.proxyUrl === options?.proxyUrl;
395
- const isCorrectProxyTier = controller.proxyTier === options?.proxyTier;
396
501
  return (isCorrectPlugin &&
397
502
  hasCapacity &&
398
- ((!controller.launchContext.browserPerProxy && !options?.proxyTier) ||
399
- (options?.proxyTier && isCorrectProxyTier) ||
503
+ (!controller.launchContext.browserPerProxy ||
400
504
  (options?.proxyUrl && isSameProxyUrl) ||
401
- (!options?.proxyUrl && !options?.proxyTier && !controller.proxyUrl && !controller.proxyTier)));
505
+ (!options?.proxyUrl && !controller.proxyUrl)));
402
506
  });
403
507
  }
404
508
  async _closeInactiveRetiredBrowsers() {
@@ -409,14 +513,14 @@ export class BrowserPool extends TypedEmitter {
409
513
  const isBrowserEmpty = controller.activePages === 0;
410
514
  if (isBrowserIdle || isBrowserEmpty) {
411
515
  const { id } = controller;
412
- log.debug('Closing retired browser.', { id });
516
+ this.log.debug('Closing retired browser.', { id });
413
517
  await controller.close();
414
518
  this.retiredBrowserControllers.delete(controller);
415
519
  closedBrowserIds.push(id);
416
520
  }
417
521
  }
418
522
  if (closedBrowserIds.length) {
419
- log.debug('Closed retired browsers.', {
523
+ this.log.debug('Closed retired browsers.', {
420
524
  count: closedBrowserIds.length,
421
525
  closedBrowserIds,
422
526
  });
@@ -429,7 +533,7 @@ export class BrowserPool extends TypedEmitter {
429
533
  page.close = async (...args) => {
430
534
  await this._executeHooks(this.prePageCloseHooks, page, browserController);
431
535
  await originalPageClose.apply(page, args).catch((err) => {
432
- log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id });
536
+ this.log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id });
433
537
  });
434
538
  await this._executeHooks(this.postPageCloseHooks, pageId, browserController);
435
539
  this.pages.delete(pageId);
@@ -447,13 +551,33 @@ export class BrowserPool extends TypedEmitter {
447
551
  // Run this with a delay, otherwise page.close()
448
552
  // might fail with "Protocol error (Target.closeTarget): Target closed."
449
553
  setTimeout(() => {
450
- log.debug('Closing retired browser because it has no active pages', { id: browserController.id });
554
+ this.log.debug('Closing retired browser because it has no active pages', { id: browserController.id });
451
555
  void browserController.close().finally(() => {
452
556
  this.retiredBrowserControllers.delete(browserController);
453
557
  });
454
558
  }, PAGE_CLOSE_KILL_TIMEOUT_MILLIS);
455
559
  }
456
560
  }
561
+ /**
562
+ * Returns `true` if the pool can accept a new browser launch without exceeding
563
+ * {@link BrowserPoolOptions.maxOpenBrowsers}. Counts starting, active, and retired browsers.
564
+ */
565
+ hasFreeBrowserSlot() {
566
+ const total = this.startingBrowserControllers.size +
567
+ this.activeBrowserControllers.size +
568
+ this.retiredBrowserControllers.size;
569
+ return total < this.maxOpenBrowsers;
570
+ }
571
+ /**
572
+ * Returns `true` if any active browser has room for another page.
573
+ */
574
+ hasActiveBrowserWithFreeCapacity() {
575
+ for (const controller of this.activeBrowserControllers) {
576
+ if (controller.activePages < this.maxOpenPagesPerBrowser)
577
+ return true;
578
+ }
579
+ return false;
580
+ }
457
581
  _initializeFingerprinting() {
458
582
  const { useFingerprintCache = true, fingerprintCacheSize = 10_000 } = this.fingerprintOptions;
459
583
  this.fingerprintGenerator = new FingerprintGenerator(this.fingerprintOptions.fingerprintGeneratorOptions);
@@ -474,4 +598,3 @@ export class BrowserPool extends TypedEmitter {
474
598
  this.postPageCreateHooks = [createPostPageCreateHook(this.fingerprintInjector), ...this.postPageCreateHooks];
475
599
  }
476
600
  }
477
- //# sourceMappingURL=browser-pool.js.map
@@ -9,4 +9,3 @@ export declare function createProxyServerForContainers(fallbackProxyUrl?: string
9
9
  ipToProxy: Map<string, string>;
10
10
  close(closeConnections: boolean): Promise<void>;
11
11
  }>;
12
- //# sourceMappingURL=container-proxy-server.d.ts.map
@@ -40,4 +40,3 @@ export async function createProxyServerForContainers(fallbackProxyUrl) {
40
40
  },
41
41
  };
42
42
  }
43
- //# sourceMappingURL=container-proxy-server.js.map
package/events.d.ts CHANGED
@@ -8,4 +8,3 @@ export declare const enum BROWSER_POOL_EVENTS {
8
8
  export declare const enum BROWSER_CONTROLLER_EVENTS {
9
9
  BROWSER_CLOSED = "browserClosed"
10
10
  }
11
- //# sourceMappingURL=events.d.ts.map
package/events.js CHANGED
@@ -10,4 +10,3 @@ export var BROWSER_CONTROLLER_EVENTS;
10
10
  (function (BROWSER_CONTROLLER_EVENTS) {
11
11
  BROWSER_CONTROLLER_EVENTS["BROWSER_CLOSED"] = "browserClosed";
12
12
  })(BROWSER_CONTROLLER_EVENTS || (BROWSER_CONTROLLER_EVENTS = {}));
13
- //# sourceMappingURL=events.js.map
@@ -14,4 +14,3 @@ export declare function createPrePageCreateHook(): (_pageId: string, browserCont
14
14
  * @internal
15
15
  */
16
16
  export declare function createPostPageCreateHook(fingerprintInjector: FingerprintInjector): (page: any, browserController: BrowserController) => Promise<void>;
17
- //# sourceMappingURL=hooks.d.ts.map
@@ -1,27 +1,39 @@
1
1
  import { PlaywrightPlugin } from '../playwright/playwright-plugin.js';
2
2
  import { PuppeteerPlugin } from '../puppeteer/puppeteer-plugin.js';
3
3
  import { getGeneratorDefaultOptions } from './utils.js';
4
+ function applySessionHints(base, fingerprint) {
5
+ if (!fingerprint)
6
+ return base;
7
+ return {
8
+ ...base,
9
+ ...(fingerprint.browser ? { browsers: [{ name: fingerprint.browser }] } : {}),
10
+ ...(fingerprint.platform ? { operatingSystems: [fingerprint.platform] } : {}),
11
+ ...(fingerprint.device ? { devices: [fingerprint.device] } : {}),
12
+ };
13
+ }
4
14
  /**
5
15
  * @internal
6
16
  */
7
17
  export function createFingerprintPreLaunchHook(browserPool) {
8
18
  const { fingerprintGenerator, fingerprintCache, fingerprintOptions: { fingerprintGeneratorOptions }, } = browserPool;
9
19
  return (_pageId, launchContext) => {
20
+ // Remote browsers may have their own fingerprinting — skip local fingerprint injection
21
+ if (launchContext.isRemote)
22
+ return;
10
23
  const { useIncognitoPages } = launchContext;
11
- const cacheKey = launchContext.session?.id ?? launchContext.proxyUrl;
24
+ const session = launchContext.session;
25
+ const cacheKey = session?.id ?? launchContext.proxyUrl;
12
26
  const { launchOptions } = launchContext;
13
- // If no options are passed we try to pass best default options as possible to match browser and OS.
14
- const fingerprintGeneratorFinalOptions = fingerprintGeneratorOptions || getGeneratorDefaultOptions(launchContext);
15
27
  let fingerprint;
16
28
  if (cacheKey && fingerprintCache?.has(cacheKey)) {
17
29
  fingerprint = fingerprintCache.get(cacheKey);
18
30
  }
19
- else if (cacheKey) {
20
- fingerprint = fingerprintGenerator.getFingerprint(fingerprintGeneratorFinalOptions);
21
- fingerprintCache?.set(cacheKey, fingerprint);
22
- }
23
31
  else {
24
- fingerprint = fingerprintGenerator.getFingerprint(fingerprintGeneratorFinalOptions);
32
+ const baseOptions = fingerprintGeneratorOptions || getGeneratorDefaultOptions(launchContext);
33
+ const finalOptions = applySessionHints(baseOptions, session?.fingerprint);
34
+ fingerprint = fingerprintGenerator.getFingerprint(finalOptions);
35
+ if (cacheKey)
36
+ fingerprintCache?.set(cacheKey, fingerprint);
25
37
  }
26
38
  launchContext.extend({ fingerprint });
27
39
  if (useIncognitoPages) {
@@ -41,6 +53,8 @@ export function createFingerprintPreLaunchHook(browserPool) {
41
53
  export function createPrePageCreateHook() {
42
54
  return (_pageId, browserController, pageOptions) => {
43
55
  const { launchContext, browserPlugin } = browserController;
56
+ if (launchContext.isRemote)
57
+ return;
44
58
  const { fingerprint } = launchContext.fingerprint;
45
59
  if (launchContext.useIncognitoPages && browserPlugin instanceof PlaywrightPlugin && pageOptions) {
46
60
  pageOptions.userAgent ??= fingerprint.navigator.userAgent;
@@ -57,6 +71,8 @@ export function createPrePageCreateHook() {
57
71
  export function createPostPageCreateHook(fingerprintInjector) {
58
72
  return async (page, browserController) => {
59
73
  const { browserPlugin, launchContext } = browserController;
74
+ if (launchContext.isRemote)
75
+ return;
60
76
  const fingerprint = launchContext.fingerprint;
61
77
  // TODO this will require refactoring, we should use common API instead of branching based on plugin type,
62
78
  // and there should be no public methods specific to some browser.
@@ -79,4 +95,3 @@ export function createPostPageCreateHook(fingerprintInjector) {
79
95
  }
80
96
  };
81
97
  }
82
- //# sourceMappingURL=hooks.js.map
@@ -61,4 +61,3 @@ export declare const enum DeviceCategory {
61
61
  desktop = "desktop"
62
62
  }
63
63
  export {};
64
- //# sourceMappingURL=types.d.ts.map
@@ -32,4 +32,3 @@ export var DeviceCategory;
32
32
  */
33
33
  DeviceCategory["desktop"] = "desktop";
34
34
  })(DeviceCategory || (DeviceCategory = {}));
35
- //# sourceMappingURL=types.js.map
@@ -1,4 +1,3 @@
1
1
  import type { LaunchContext } from '../launch-context.js';
2
2
  import type { FingerprintGeneratorOptions } from './types.js';
3
3
  export declare const getGeneratorDefaultOptions: (launchContext: LaunchContext) => FingerprintGeneratorOptions;
4
- //# sourceMappingURL=utils.d.ts.map
@@ -42,4 +42,3 @@ const getOperatingSystem = () => {
42
42
  return "linux" /* OperatingSystemsName.linux */;
43
43
  }
44
44
  };
45
- //# sourceMappingURL=utils.js.map
package/index.d.ts CHANGED
@@ -28,13 +28,20 @@ export * from './browser-pool.js';
28
28
  export * from './playwright/playwright-plugin.js';
29
29
  export * from './puppeteer/puppeteer-plugin.js';
30
30
  export * from './events.js';
31
- export { BrowserName, DeviceCategory, OperatingSystemsName, } from './fingerprinting/types.js';
32
- export { BrowserController, BrowserControllerEvents } from './abstract-classes/browser-controller.js';
31
+ export type { BrowserSpecification, FingerprintGenerator, FingerprintGeneratorOptions, GetFingerprintReturn, } from './fingerprinting/types.js';
32
+ export { BrowserName, DeviceCategory, OperatingSystemsName } from './fingerprinting/types.js';
33
+ export type { BrowserControllerEvents, IBrowserController, IBrowserLaunchContext, } from './abstract-classes/browser-controller.js';
34
+ export { BrowserController } from './abstract-classes/browser-controller.js';
33
35
  export { PuppeteerController } from './puppeteer/puppeteer-controller.js';
34
36
  export { PlaywrightController } from './playwright/playwright-controller.js';
35
37
  export { PlaywrightBrowser } from './playwright/playwright-browser.js';
36
- export { CommonPage, CommonLibrary, BrowserPlugin, BrowserPluginOptions, CreateLaunchContextOptions, BrowserLaunchError, DEFAULT_USER_AGENT, } from './abstract-classes/browser-plugin.js';
37
- export { LaunchContext, LaunchContextOptions } from './launch-context.js';
38
- export { BrowserSpecification, FingerprintGenerator, FingerprintGeneratorOptions, GetFingerprintReturn, } from './fingerprinting/types.js';
39
- export { InferBrowserPluginArray, UnwrapPromise } from './utils.js';
40
- //# sourceMappingURL=index.d.ts.map
38
+ export type { CommonPage, CommonLibrary, BrowserPluginOptions, CreateLaunchContextOptions, } from './abstract-classes/browser-plugin.js';
39
+ export { BrowserPlugin, BrowserLaunchError, DEFAULT_USER_AGENT } from './abstract-classes/browser-plugin.js';
40
+ export type { LaunchContextOptions } from './launch-context.js';
41
+ export { LaunchContext } from './launch-context.js';
42
+ export { RemoteBrowserProvider } from './remote-browser-provider.js';
43
+ export { RemoteBrowserPool } from './remote-browser-pool.js';
44
+ export type { RemoteBrowserPoolOptions, CrawlerRemoteBrowserOptions, RemoteBrowserEndpoint, ResolvedRemoteEndpoint, RemoteConnection, RemoteConnectionParameters, } from './remote-browser-pool.js';
45
+ export type { InferBrowserPluginArray, UnwrapPromise } from './utils.js';
46
+ export { anonymizeProxySugar, type AnonymizeProxySugarOptions } from './anonymize-proxy.js';
47
+ export type { IBrowserPool, NewPageOptions } from '@crawlee/types';
package/index.js CHANGED
@@ -26,11 +26,13 @@ export * from './browser-pool.js';
26
26
  export * from './playwright/playwright-plugin.js';
27
27
  export * from './puppeteer/puppeteer-plugin.js';
28
28
  export * from './events.js';
29
- export { BrowserName, DeviceCategory, OperatingSystemsName, } from './fingerprinting/types.js';
29
+ export { BrowserName, DeviceCategory, OperatingSystemsName } from './fingerprinting/types.js';
30
30
  export { BrowserController } from './abstract-classes/browser-controller.js';
31
31
  export { PuppeteerController } from './puppeteer/puppeteer-controller.js';
32
32
  export { PlaywrightController } from './playwright/playwright-controller.js';
33
33
  export { PlaywrightBrowser } from './playwright/playwright-browser.js';
34
- export { BrowserPlugin, BrowserLaunchError, DEFAULT_USER_AGENT, } from './abstract-classes/browser-plugin.js';
34
+ export { BrowserPlugin, BrowserLaunchError, DEFAULT_USER_AGENT } from './abstract-classes/browser-plugin.js';
35
35
  export { LaunchContext } from './launch-context.js';
36
- //# sourceMappingURL=index.js.map
36
+ export { RemoteBrowserProvider } from './remote-browser-provider.js';
37
+ export { RemoteBrowserPool } from './remote-browser-pool.js';
38
+ export { anonymizeProxySugar } from './anonymize-proxy.js';
@@ -43,7 +43,17 @@ export interface LaunchContextOptions<Library extends CommonLibrary = CommonLibr
43
43
  */
44
44
  userDataDir?: string;
45
45
  proxyUrl?: string;
46
- proxyTier?: number;
46
+ /**
47
+ * If set to `true`, TLS certificate errors from the upstream proxy will be ignored.
48
+ * This is useful when using HTTPS proxies with self-signed certificates.
49
+ */
50
+ ignoreProxyCertificate?: boolean;
51
+ /**
52
+ * Whether this launch context represents a connection to a remote browser
53
+ * rather than a locally launched one.
54
+ * @default false
55
+ */
56
+ isRemote?: boolean;
47
57
  }
48
58
  export declare class LaunchContext<Library extends CommonLibrary = CommonLibrary, LibraryOptions extends Dictionary | undefined = Parameters<Library['launch']>[0], LaunchResult extends CommonBrowser = UnwrapPromise<ReturnType<Library['launch']>>, NewPageOptions = Parameters<LaunchResult['newPage']>[0], NewPageResult = UnwrapPromise<ReturnType<LaunchResult['newPage']>>> {
49
59
  id?: string;
@@ -52,10 +62,17 @@ export declare class LaunchContext<Library extends CommonLibrary = CommonLibrary
52
62
  useIncognitoPages: boolean;
53
63
  browserPerProxy?: boolean;
54
64
  userDataDir: string;
55
- proxyTier?: number;
65
+ readonly isRemote: boolean;
66
+ ignoreProxyCertificate?: boolean;
56
67
  private _proxyUrl?;
57
68
  private readonly _reservedFieldNames;
58
69
  fingerprint?: BrowserFingerprintWithHeaders;
70
+ /**
71
+ * Token identifying the remote browser session this context connected to, set by the plugin and read by
72
+ * the {@link RemoteBrowserPool} to release the session on close. Only present for remote connections.
73
+ * @internal
74
+ */
75
+ _remoteToken?: number;
59
76
  [K: PropertyKey]: unknown;
60
77
  constructor(options: LaunchContextOptions<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>);
61
78
  /**
@@ -76,4 +93,3 @@ export declare class LaunchContext<Library extends CommonLibrary = CommonLibrary
76
93
  */
77
94
  get proxyUrl(): string | undefined;
78
95
  }
79
- //# sourceMappingURL=launch-context.d.ts.map
package/launch-context.js CHANGED
@@ -5,19 +5,27 @@ export class LaunchContext {
5
5
  useIncognitoPages;
6
6
  browserPerProxy;
7
7
  userDataDir;
8
- proxyTier;
8
+ isRemote;
9
+ ignoreProxyCertificate;
9
10
  _proxyUrl;
10
11
  _reservedFieldNames = [...Reflect.ownKeys(this), 'extend'];
11
12
  fingerprint;
13
+ /**
14
+ * Token identifying the remote browser session this context connected to, set by the plugin and read by
15
+ * the {@link RemoteBrowserPool} to release the session on close. Only present for remote connections.
16
+ * @internal
17
+ */
18
+ _remoteToken;
12
19
  constructor(options) {
13
- const { id, browserPlugin, launchOptions, proxyUrl, useIncognitoPages, browserPerProxy, userDataDir = '', proxyTier, } = options;
20
+ const { id, browserPlugin, launchOptions, proxyUrl, useIncognitoPages, browserPerProxy, userDataDir = '', ignoreProxyCertificate, isRemote, } = options;
14
21
  this.id = id;
15
22
  this.browserPlugin = browserPlugin;
16
23
  this.launchOptions = launchOptions;
17
24
  this.browserPerProxy = browserPerProxy ?? false;
18
25
  this.useIncognitoPages = useIncognitoPages ?? false;
19
26
  this.userDataDir = userDataDir;
20
- this.proxyTier = proxyTier;
27
+ this.ignoreProxyCertificate = ignoreProxyCertificate ?? false;
28
+ this.isRemote = isRemote ?? false;
21
29
  this._proxyUrl = proxyUrl;
22
30
  }
23
31
  /**
@@ -43,6 +51,7 @@ export class LaunchContext {
43
51
  */
44
52
  set proxyUrl(url) {
45
53
  if (!url) {
54
+ this._proxyUrl = undefined;
46
55
  return;
47
56
  }
48
57
  const urlInstance = new URL(url);
@@ -59,4 +68,3 @@ export class LaunchContext {
59
68
  return this._proxyUrl;
60
69
  }
61
70
  }
62
- //# sourceMappingURL=launch-context.js.map