@browserless.io/browserless 2.1.1 → 2.2.0-beta-3

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 (38) hide show
  1. package/bin/scaffold/tsconfig.json +1 -1
  2. package/build/browsers/index.d.ts +1 -10
  3. package/build/browsers/index.js +19 -29
  4. package/build/router.js +13 -6
  5. package/build/routes/chromium/http/content-post.body.json +15 -19
  6. package/build/routes/chromium/http/content-post.d.ts +1 -1
  7. package/build/routes/chromium/http/content-post.js +2 -4
  8. package/build/routes/chromium/http/pdf-post.body.json +15 -19
  9. package/build/routes/chromium/http/pdf-post.d.ts +1 -1
  10. package/build/routes/chromium/http/pdf-post.js +10 -6
  11. package/build/routes/chromium/http/scrape-post.body.json +15 -19
  12. package/build/routes/chromium/http/scrape-post.d.ts +3 -3
  13. package/build/routes/chromium/http/scrape-post.js +2 -4
  14. package/build/routes/chromium/http/scrape-post.response.json +22 -38
  15. package/build/routes/chromium/http/screenshot-post.body.json +15 -19
  16. package/build/routes/chromium/http/screenshot-post.d.ts +1 -1
  17. package/build/routes/chromium/http/screenshot-post.js +2 -4
  18. package/build/routes/chromium/tests/content.spec.js +27 -1
  19. package/build/routes/chromium/tests/websocket.spec.js +53 -4
  20. package/build/routes/chromium/ws/browser.js +1 -1
  21. package/build/routes/management/http/sessions-get.response.json +4 -0
  22. package/build/types.d.ts +1 -0
  23. package/build/utils.d.ts +1 -0
  24. package/build/utils.js +13 -0
  25. package/package.json +3 -3
  26. package/src/browsers/index.ts +24 -45
  27. package/src/router.ts +13 -7
  28. package/src/routes/chromium/http/content-post.ts +3 -4
  29. package/src/routes/chromium/http/pdf-post.ts +13 -6
  30. package/src/routes/chromium/http/scrape-post.ts +5 -6
  31. package/src/routes/chromium/http/screenshot-post.ts +3 -4
  32. package/src/routes/chromium/tests/content.spec.ts +28 -1
  33. package/src/routes/chromium/tests/websocket.spec.ts +70 -4
  34. package/src/routes/chromium/ws/browser.ts +1 -1
  35. package/src/types.ts +1 -0
  36. package/src/utils.ts +26 -0
  37. package/static/docs/swagger.json +70 -98
  38. package/static/function/client.js +192 -488
@@ -109,7 +109,7 @@
109
109
  {
110
110
  "type": "array",
111
111
  "items": {
112
- "$ref": "#/definitions/Protocol.Network.Cookie"
112
+ "$ref": "#/definitions/Cookie"
113
113
  }
114
114
  },
115
115
  {
@@ -175,8 +175,8 @@
175
175
  "debug"
176
176
  ],
177
177
  "definitions": {
178
- "Protocol.Network.Cookie": {
179
- "description": "Cookie object",
178
+ "Cookie": {
179
+ "description": "Represents a cookie object.",
180
180
  "type": "object",
181
181
  "properties": {
182
182
  "name": {
@@ -196,12 +196,12 @@
196
196
  "type": "string"
197
197
  },
198
198
  "expires": {
199
- "description": "Cookie expiration date as the number of seconds since the UNIX epoch.",
199
+ "description": "Cookie expiration date as the number of seconds since the UNIX epoch. Set to `-1` for\nsession cookies",
200
200
  "type": "number"
201
201
  },
202
202
  "size": {
203
203
  "description": "Cookie size.",
204
- "type": "integer"
204
+ "type": "number"
205
205
  },
206
206
  "httpOnly": {
207
207
  "description": "True if cookie is http-only.",
@@ -225,27 +225,33 @@
225
225
  "type": "string"
226
226
  },
227
227
  "priority": {
228
- "$ref": "#/definitions/Protocol.Network.CookiePriority",
229
- "description": "Cookie Priority"
228
+ "description": "Cookie Priority. Supported only in Chrome.",
229
+ "enum": [
230
+ "High",
231
+ "Low",
232
+ "Medium"
233
+ ],
234
+ "type": "string"
230
235
  },
231
236
  "sameParty": {
232
- "description": "True if cookie is SameParty.",
237
+ "description": "True if cookie is SameParty. Supported only in Chrome.",
233
238
  "type": "boolean"
234
239
  },
235
240
  "sourceScheme": {
236
- "$ref": "#/definitions/Protocol.Network.CookieSourceScheme",
237
- "description": "Cookie source scheme type."
238
- },
239
- "sourcePort": {
240
- "description": "Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\nThis is a temporary ability and it will be removed in the future.",
241
- "type": "integer"
241
+ "description": "Cookie source scheme type. Supported only in Chrome.",
242
+ "enum": [
243
+ "NonSecure",
244
+ "Secure",
245
+ "Unset"
246
+ ],
247
+ "type": "string"
242
248
  },
243
249
  "partitionKey": {
244
- "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the start\nof the request to the endpoint that set the cookie.",
250
+ "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the\nstart of the request to the endpoint that set the cookie. Supported only in Chrome.",
245
251
  "type": "string"
246
252
  },
247
253
  "partitionKeyOpaque": {
248
- "description": "True if cookie partition key is opaque.",
254
+ "description": "True if cookie partition key is opaque. Supported only in Chrome.",
249
255
  "type": "boolean"
250
256
  }
251
257
  },
@@ -256,34 +262,12 @@
256
262
  "httpOnly",
257
263
  "name",
258
264
  "path",
259
- "priority",
260
- "sameParty",
261
265
  "secure",
262
266
  "session",
263
267
  "size",
264
- "sourcePort",
265
- "sourceScheme",
266
268
  "value"
267
269
  ]
268
270
  },
269
- "Protocol.Network.CookiePriority": {
270
- "description": "Represents the cookie's 'Priority' status:\nhttps://tools.ietf.org/html/draft-west-cookie-priority-00",
271
- "enum": [
272
- "High",
273
- "Low",
274
- "Medium"
275
- ],
276
- "type": "string"
277
- },
278
- "Protocol.Network.CookieSourceScheme": {
279
- "description": "Represents the source scheme of the origin that originally set the cookie.\nA value of \"Unset\" allows protocol clients to emulate legacy cookie scope for the scheme.\nThis is a temporary ability and it will be removed in the future.",
280
- "enum": [
281
- "NonSecure",
282
- "Secure",
283
- "Unset"
284
- ],
285
- "type": "string"
286
- },
287
271
  "InBoundRequest": {
288
272
  "type": "object",
289
273
  "properties": {
@@ -23,7 +23,7 @@
23
23
  "cookies": {
24
24
  "type": "array",
25
25
  "items": {
26
- "$ref": "#/definitions/Protocol.Network.CookieParam"
26
+ "$ref": "#/definitions/CookieParam"
27
27
  }
28
28
  },
29
29
  "emulateMediaType": {
@@ -238,7 +238,7 @@
238
238
  "username"
239
239
  ]
240
240
  },
241
- "Protocol.Network.CookieParam": {
241
+ "CookieParam": {
242
242
  "description": "Cookie parameter object",
243
243
  "type": "object",
244
244
  "properties": {
@@ -251,7 +251,7 @@
251
251
  "type": "string"
252
252
  },
253
253
  "url": {
254
- "description": "The request-URI to associate with the setting of the cookie. This value can affect the\ndefault domain, path, source port, and source scheme values of the created cookie.",
254
+ "description": "The request-URI to associate with the setting of the cookie. This value can affect\nthe default domain, path, and source scheme values of the created cookie.",
255
255
  "type": "string"
256
256
  },
257
257
  "domain": {
@@ -284,7 +284,7 @@
284
284
  "type": "number"
285
285
  },
286
286
  "priority": {
287
- "description": "Cookie Priority.",
287
+ "description": "Cookie Priority. Supported only in Chrome.",
288
288
  "enum": [
289
289
  "High",
290
290
  "Low",
@@ -293,11 +293,11 @@
293
293
  "type": "string"
294
294
  },
295
295
  "sameParty": {
296
- "description": "True if cookie is SameParty.",
296
+ "description": "True if cookie is SameParty. Supported only in Chrome.",
297
297
  "type": "boolean"
298
298
  },
299
299
  "sourceScheme": {
300
- "description": "Cookie source scheme type.",
300
+ "description": "Cookie source scheme type. Supported only in Chrome.",
301
301
  "enum": [
302
302
  "NonSecure",
303
303
  "Secure",
@@ -305,12 +305,8 @@
305
305
  ],
306
306
  "type": "string"
307
307
  },
308
- "sourcePort": {
309
- "description": "Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\nThis is a temporary ability and it will be removed in the future.",
310
- "type": "number"
311
- },
312
308
  "partitionKey": {
313
- "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the start\nof the request to the endpoint that set the cookie.\nIf not set, the cookie will be set as not partitioned.",
309
+ "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the\nstart of the request to the endpoint that set the cookie. If not set, the cookie will\nbe set as not partitioned.",
314
310
  "type": "string"
315
311
  }
316
312
  },
@@ -488,14 +484,14 @@
488
484
  "length": {
489
485
  "type": "number"
490
486
  },
491
- "__@toStringTag@118386": {
487
+ "__@toStringTag@96609": {
492
488
  "type": "string",
493
489
  "const": "Uint8Array"
494
490
  }
495
491
  },
496
492
  "required": [
497
493
  "BYTES_PER_ELEMENT",
498
- "__@toStringTag@118386",
494
+ "__@toStringTag@96609",
499
495
  "buffer",
500
496
  "byteLength",
501
497
  "byteOffset",
@@ -530,13 +526,13 @@
530
526
  "byteLength": {
531
527
  "type": "number"
532
528
  },
533
- "__@toStringTag@118386": {
529
+ "__@toStringTag@96609": {
534
530
  "type": "string"
535
531
  }
536
532
  },
537
533
  "additionalProperties": false,
538
534
  "required": [
539
- "__@toStringTag@118386",
535
+ "__@toStringTag@96609",
540
536
  "byteLength"
541
537
  ]
542
538
  },
@@ -546,18 +542,18 @@
546
542
  "byteLength": {
547
543
  "type": "number"
548
544
  },
549
- "__@species@118487": {
545
+ "__@species@96710": {
550
546
  "$ref": "#/definitions/SharedArrayBuffer"
551
547
  },
552
- "__@toStringTag@118386": {
548
+ "__@toStringTag@96609": {
553
549
  "type": "string",
554
550
  "const": "SharedArrayBuffer"
555
551
  }
556
552
  },
557
553
  "additionalProperties": false,
558
554
  "required": [
559
- "__@species@118487",
560
- "__@toStringTag@118386",
555
+ "__@species@96710",
556
+ "__@toStringTag@96609",
561
557
  "byteLength"
562
558
  ]
563
559
  },
@@ -33,7 +33,7 @@ export interface BodySchema {
33
33
  waitForEvent?: WaitForEventOptions;
34
34
  waitForFunction?: WaitForFunctionOptions;
35
35
  waitForSelector?: WaitForSelectorOptions;
36
- waitForTimeout?: Parameters<Page['waitForTimeout']>[0];
36
+ waitForTimeout?: number;
37
37
  }
38
38
  export default class ScreenshotPost extends BrowserHTTPRoute {
39
39
  accepts: contentTypes[];
@@ -1,4 +1,4 @@
1
- import { APITags, BadRequest, BrowserHTTPRoute, CDPChromium, HTTPRoutes, Methods, bestAttemptCatch, contentTypes, dedent, noop, scrollThroughPage, waitForEvent as waitForEvt, waitForFunction as waitForFn, } from '@browserless.io/browserless';
1
+ import { APITags, BadRequest, BrowserHTTPRoute, CDPChromium, HTTPRoutes, Methods, bestAttemptCatch, contentTypes, dedent, noop, scrollThroughPage, sleep, waitForEvent as waitForEvt, waitForFunction as waitForFn, } from '@browserless.io/browserless';
2
2
  import Stream from 'stream';
3
3
  export default class ScreenshotPost extends BrowserHTTPRoute {
4
4
  accepts = [contentTypes.json];
@@ -81,9 +81,7 @@ export default class ScreenshotPost extends BrowserHTTPRoute {
81
81
  }
82
82
  const gotoResponse = await gotoCall(content, gotoOptions).catch(bestAttemptCatch(bestAttempt));
83
83
  if (waitForTimeout) {
84
- await page
85
- .waitForTimeout(waitForTimeout)
86
- .catch(bestAttemptCatch(bestAttempt));
84
+ await sleep(waitForTimeout).catch(bestAttemptCatch(bestAttempt));
87
85
  }
88
86
  if (waitForFunction) {
89
87
  await waitForFn(page, waitForFunction).catch(bestAttemptCatch(bestAttempt));
@@ -1,4 +1,4 @@
1
- import { Browserless, Config, Metrics } from '@browserless.io/browserless';
1
+ import { Browserless, Config, Metrics, sleep } from '@browserless.io/browserless';
2
2
  import { expect } from 'chai';
3
3
  describe('/content API', function () {
4
4
  let browserless;
@@ -32,6 +32,32 @@ describe('/content API', function () {
32
32
  expect(res.status).to.equal(200);
33
33
  });
34
34
  });
35
+ it('cancels request when they are closed early', async () => {
36
+ const config = new Config();
37
+ const metrics = new Metrics();
38
+ await start({ config, metrics });
39
+ const body = {
40
+ url: 'https://cnn.com',
41
+ };
42
+ const controller = new AbortController();
43
+ const signal = controller.signal;
44
+ const promise = fetch('http://localhost:3000/content', {
45
+ body: JSON.stringify(body),
46
+ headers: {
47
+ 'content-type': 'application/json',
48
+ },
49
+ method: 'POST',
50
+ signal,
51
+ }).catch(async (error) => {
52
+ await sleep(100);
53
+ expect(error).to.have.property('name', 'AbortError');
54
+ expect(metrics.get().error).to.equal(1);
55
+ expect(metrics.get().successful).to.equal(0);
56
+ });
57
+ await sleep(1000);
58
+ controller.abort();
59
+ return promise;
60
+ });
35
61
  it('404s GET requests', async () => {
36
62
  const config = new Config();
37
63
  config.setToken('browserless');
@@ -1,4 +1,4 @@
1
- import { Browserless, Config, Metrics, exists, sleep, } from '@browserless.io/browserless';
1
+ import { Browserless, Config, Metrics, exists, fetchJson, sleep, } from '@browserless.io/browserless';
2
2
  import { chromium } from 'playwright-core';
3
3
  import { deleteAsync } from 'del';
4
4
  import { expect } from 'chai';
@@ -56,6 +56,54 @@ describe('WebSocket API', function () {
56
56
  });
57
57
  await Promise.all([browser.disconnect(), browserTwo.disconnect()]);
58
58
  });
59
+ it('does not close browsers when multiple clients are connected', async () => {
60
+ const config = new Config();
61
+ config.setToken('browserless');
62
+ const metrics = new Metrics();
63
+ await start({ config, metrics });
64
+ // Single session
65
+ const browser = await puppeteer.connect({
66
+ browserWSEndpoint: `ws://localhost:3000?token=browserless`,
67
+ });
68
+ const [session] = (await fetchJson('http://localhost:3000/sessions?token=browserless'));
69
+ expect(session.numbConnected).to.equal(1);
70
+ // Two sessions
71
+ const browserTwo = await puppeteer.connect({
72
+ browserWSEndpoint: `ws://localhost:3000/devtools/browser/${session.browserId}?token=browserless`,
73
+ });
74
+ const [twoSessions] = (await fetchJson('http://localhost:3000/sessions?token=browserless'));
75
+ expect(twoSessions.numbConnected).to.equal(2);
76
+ // Back to a single session
77
+ await browser.disconnect();
78
+ await sleep(50);
79
+ const [oneSession] = (await fetchJson('http://localhost:3000/sessions?token=browserless'));
80
+ expect(oneSession.numbConnected).to.equal(1);
81
+ // No sessions connected
82
+ await browserTwo.disconnect();
83
+ await sleep(50);
84
+ const sessionsFinal = (await fetchJson('http://localhost:3000/sessions?token=browserless'));
85
+ expect(sessionsFinal).to.have.length(0);
86
+ });
87
+ it('disconnects all clients when the timeout is reached', async () => {
88
+ const config = new Config();
89
+ config.setToken('browserless');
90
+ config.setTimeout(1000);
91
+ config.setConcurrent(2);
92
+ const metrics = new Metrics();
93
+ await start({ config, metrics });
94
+ const browser = await puppeteer.connect({
95
+ browserWSEndpoint: `ws://localhost:3000?token=browserless`,
96
+ });
97
+ const [session] = (await fetchJson('http://localhost:3000/sessions?token=browserless'));
98
+ const browserTwo = await puppeteer.connect({
99
+ browserWSEndpoint: `ws://localhost:3000/devtools/browser/${session.browserId}?token=browserless`,
100
+ });
101
+ await sleep(3000);
102
+ expect(metrics.get().successful).to.equal(0);
103
+ expect(metrics.get().timedout).to.equal(2);
104
+ expect(browser.connected).to.be.false;
105
+ expect(browserTwo.connected).to.be.false;
106
+ });
59
107
  it('rejects websocket requests', async () => {
60
108
  const config = new Config();
61
109
  config.setToken('browserless');
@@ -189,12 +237,13 @@ describe('WebSocket API', function () {
189
237
  const metrics = new Metrics();
190
238
  await start({ config, metrics });
191
239
  const browser = await puppeteer.connect({
192
- browserWSEndpoint: `ws://localhost:3000?timeout=500&token=browserless`,
240
+ browserWSEndpoint: `ws://localhost:3000?timeout=1000&token=browserless`,
193
241
  });
194
- await sleep(750);
195
- browser.disconnect();
242
+ expect(browser.connected).to.be.true;
243
+ await sleep(1200);
196
244
  expect(metrics.get().timedout).to.equal(1);
197
245
  expect(metrics.get().successful).to.equal(0);
246
+ expect(browser.connected).to.be.false;
198
247
  });
199
248
  it('allows the file-chooser', async () => new Promise(async (done) => {
200
249
  const config = new Config();
@@ -2,7 +2,7 @@ import { APITags, BrowserWebsocketRoute, CDPChromium, WebsocketRoutes, } from '@
2
2
  export default class CDPExistingBrowser extends BrowserWebsocketRoute {
3
3
  auth = true;
4
4
  browser = CDPChromium;
5
- concurrency = false;
5
+ concurrency = true;
6
6
  description = `Connect to an already-running Chromium with a library like puppeteer, or others, that work over chrome-devtools-protocol.`;
7
7
  path = WebsocketRoutes.browser;
8
8
  tags = [APITags.browserWS];
@@ -10,6 +10,9 @@
10
10
  "browser": {
11
11
  "type": "string"
12
12
  },
13
+ "browserId": {
14
+ "type": "string"
15
+ },
13
16
  "id": {
14
17
  "type": [
15
18
  "null",
@@ -57,6 +60,7 @@
57
60
  "additionalProperties": false,
58
61
  "required": [
59
62
  "browser",
63
+ "browserId",
60
64
  "id",
61
65
  "initialConnectURL",
62
66
  "killURL",
package/build/types.d.ts CHANGED
@@ -274,6 +274,7 @@ export interface BrowserlessSession {
274
274
  }
275
275
  export interface BrowserlessSessionJSON {
276
276
  browser: string;
277
+ browserId: string;
277
278
  id: string | null;
278
279
  initialConnectURL: string;
279
280
  killURL: string | null;
package/build/utils.d.ts CHANGED
@@ -32,6 +32,7 @@ export declare const readRequestBody: (req: Request) => Promise<string>;
32
32
  export declare const safeParse: (maybeJson: string) => unknown | null;
33
33
  export declare const removeNullStringify: (json: unknown, allowNull?: boolean) => string;
34
34
  export declare const jsonOrString: (maybeJson: string) => unknown | string;
35
+ export declare const generateDataDir: (sessionId: string | undefined, config: Config) => Promise<string>;
35
36
  export declare const readBody: (req: Request) => Promise<ReturnType<typeof safeParse>>;
36
37
  export declare const getRouteFiles: (config: Config) => Promise<string[][]>;
37
38
  export declare const make404: (...messages: string[]) => string;
package/build/utils.js CHANGED
@@ -176,6 +176,19 @@ export const removeNullStringify = (json, allowNull = true) => {
176
176
  }, ' ');
177
177
  };
178
178
  export const jsonOrString = (maybeJson) => safeParse(maybeJson) ?? maybeJson;
179
+ export const generateDataDir = async (sessionId = id(), config) => {
180
+ const baseDirectory = await config.getDataDir();
181
+ const dataDirPath = path.join(baseDirectory, `browserless-data-dir-${sessionId}`);
182
+ if (await exists(dataDirPath)) {
183
+ debug(`Data directory already exists, not creating "${dataDirPath}"`);
184
+ return dataDirPath;
185
+ }
186
+ debug(`Generating user-data-dir at ${dataDirPath}`);
187
+ await fs.mkdir(dataDirPath, { recursive: true }).catch((err) => {
188
+ throw new ServerError(`Error creating data-directory "${dataDirPath}": ${err}`);
189
+ });
190
+ return dataDirPath;
191
+ };
179
192
  export const readBody = async (req) => {
180
193
  if (typeof req.body === 'string' &&
181
194
  (isBase64.test(req.body) || req.body.startsWith('{'))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserless.io/browserless",
3
- "version": "2.1.1",
3
+ "version": "2.2.0-beta-3",
4
4
  "license": "SSPL",
5
5
  "description": "The browserless platform",
6
6
  "author": "browserless.io",
@@ -56,7 +56,7 @@
56
56
  "lighthouse": "^11.1.0",
57
57
  "micromatch": "^4.0.4",
58
58
  "playwright-core": "^1.41.2",
59
- "puppeteer-core": "^21.10.0",
59
+ "puppeteer-core": "^22.0.0",
60
60
  "puppeteer-extra": "^3.3.6",
61
61
  "puppeteer-extra-plugin-stealth": "^2.11.2",
62
62
  "queue": "^7.0.0",
@@ -71,7 +71,7 @@
71
71
  "@types/mocha": "^10.0.6",
72
72
  "@types/node": "^20.11.16",
73
73
  "@types/sinon": "^17.0.3",
74
- "@typescript-eslint/eslint-plugin": "^6.20.0",
74
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
75
75
  "@typescript-eslint/parser": "^6.21.0",
76
76
  "assert": "^2.0.0",
77
77
  "chai": "^5.0.3",
@@ -11,27 +11,33 @@ import {
11
11
  Config,
12
12
  HTTPManagementRoutes,
13
13
  NotFound,
14
+ PlaywrightChromium,
15
+ PlaywrightFirefox,
16
+ PlaywrightWebkit,
14
17
  Request,
15
- ServerError,
16
18
  browserHook,
17
19
  convertIfBase64,
18
20
  createLogger,
19
21
  exists,
20
- id,
22
+ generateDataDir,
21
23
  makeExternalURL,
22
24
  noop,
23
25
  pageHook,
24
26
  parseBooleanParam,
25
27
  } from '@browserless.io/browserless';
26
- import path, { join } from 'path';
27
28
  import { deleteAsync } from 'del';
28
- import { mkdir } from 'fs/promises';
29
+ import path from 'path';
29
30
 
30
31
  export class BrowserManager {
31
32
  protected browsers: Map<BrowserInstance, BrowserlessSession> = new Map();
32
33
  protected launching: Map<string, Promise<unknown>> = new Map();
33
34
  protected timers: Map<string, number> = new Map();
34
35
  protected debug = createLogger('browser-manager');
36
+ protected playwrightBrowserNames = [
37
+ PlaywrightChromium.name,
38
+ PlaywrightFirefox.name,
39
+ PlaywrightWebkit.name,
40
+ ];
35
41
 
36
42
  constructor(protected config: Config) {}
37
43
 
@@ -46,42 +52,6 @@ export class BrowserManager {
46
52
  }
47
53
  };
48
54
 
49
- /**
50
- * Generates a directory for the user-data-dir contents to be saved in. Uses
51
- * the provided sessionId, or creates one when omitted,
52
- * and appends it to the name of the directory. If the
53
- * directory already exists then no action is taken, verified by run `stat`
54
- *
55
- * @param sessionId The ID of the session
56
- * @returns Promise<string> of the fully-qualified path of the directory
57
- */
58
- protected generateDataDir = async (
59
- sessionId: string = id(),
60
- ): Promise<string> => {
61
- const baseDirectory = await this.config.getDataDir();
62
- const dataDirPath = join(
63
- baseDirectory,
64
- `browserless-data-dir-${sessionId}`,
65
- );
66
-
67
- if (await exists(dataDirPath)) {
68
- this.debug(
69
- `Data directory already exists, not creating "${dataDirPath}"`,
70
- );
71
- return dataDirPath;
72
- }
73
-
74
- this.debug(`Generating user-data-dir at ${dataDirPath}`);
75
-
76
- await mkdir(dataDirPath, { recursive: true }).catch((err) => {
77
- throw new ServerError(
78
- `Error creating data-directory "${dataDirPath}": ${err}`,
79
- );
80
- });
81
-
82
- return dataDirPath;
83
- };
84
-
85
55
  public getProtocolJSON = async (): Promise<object> => {
86
56
  this.debug(`Launching Chrome to generate /json/protocol results`);
87
57
  const browser = new CDPChromium({
@@ -155,7 +125,7 @@ export class BrowserManager {
155
125
  {
156
126
  ...session,
157
127
  browser: browser.constructor.name,
158
- browserId: browser.wsEndpoint()?.split('/').pop(),
128
+ browserId: browser.wsEndpoint()?.split('/').pop() as string,
159
129
  initialConnectURL: new URL(session.initialConnectURL, serverAddress)
160
130
  .href,
161
131
  killURL: session.id
@@ -200,6 +170,11 @@ export class BrowserManager {
200
170
  const cleanupACtions: Array<() => Promise<void>> = [];
201
171
  this.debug(`${session.numbConnected} Client(s) are currently connected`);
202
172
 
173
+ // Don't close if there's clients still connected
174
+ if (session.numbConnected > 0) {
175
+ return;
176
+ }
177
+
203
178
  this.debug(`Closing browser session`);
204
179
  cleanupACtions.push(() => browser.close());
205
180
 
@@ -264,13 +239,15 @@ export class BrowserManager {
264
239
  if (req.parsed.pathname.includes('/devtools/browser')) {
265
240
  const sessions = Array.from(this.browsers);
266
241
  const id = req.parsed.pathname.split('/').pop() as string;
267
- const browser = sessions.find(([b]) =>
242
+ const found = sessions.find(([b]) =>
268
243
  b.wsEndpoint()?.includes(req.parsed.pathname),
269
244
  );
270
245
 
271
- if (browser) {
246
+ if (found) {
247
+ const [browser, session] = found;
248
+ ++session.numbConnected;
272
249
  this.debug(`Located browser with ID ${id}`);
273
- return browser[0];
250
+ return browser;
274
251
  }
275
252
 
276
253
  throw new NotFound(
@@ -305,7 +282,9 @@ export class BrowserManager {
305
282
  // unless it's playwright which takes care of its own data-dirs
306
283
  const userDataDir =
307
284
  manualUserDataDir ||
308
- (Browser.name === CDPChromium.name ? await this.generateDataDir() : null);
285
+ (!this.playwrightBrowserNames.includes(Browser.name)
286
+ ? await generateDataDir(undefined, this.config)
287
+ : null);
309
288
 
310
289
  const proxyServerArg = launchOptions.args?.find((arg) =>
311
290
  arg.includes('--proxy-server='),
package/src/router.ts CHANGED
@@ -80,17 +80,23 @@ export class Router {
80
80
  }
81
81
 
82
82
  if (!browser) {
83
- return writeResponse(res, 500, `Error loading the browser.`);
84
- }
85
-
86
- if (!isConnected(res)) {
87
- this.log(`HTTP Request has closed prior to running`);
88
- return Promise.resolve();
83
+ return writeResponse(res, 500, `Error loading the browser`);
89
84
  }
90
85
 
91
86
  try {
92
87
  this.verbose(`Running found HTTP handler.`);
93
- return await handler(req, res, browser);
88
+ return await Promise.race([
89
+ handler(req, res, browser),
90
+ new Promise((resolve, reject) => {
91
+ res.once('close', () => {
92
+ if (!res.writableEnded) {
93
+ reject(new Error(`Request closed prior to writing results`));
94
+ }
95
+ this.verbose(`Response has been written, resolving`);
96
+ resolve(null);
97
+ });
98
+ }),
99
+ ]);
94
100
  } finally {
95
101
  this.verbose(`HTTP Request handler has finished.`);
96
102
  this.browserManager.complete(browser);
@@ -21,6 +21,7 @@ import {
21
21
  rejectResourceTypes,
22
22
  requestInterceptors,
23
23
  setJavaScriptEnabled,
24
+ sleep,
24
25
  waitForEvent as waitForEvt,
25
26
  waitForFunction as waitForFn,
26
27
  writeResponse,
@@ -48,7 +49,7 @@ export interface BodySchema {
48
49
  waitForEvent?: WaitForEventOptions;
49
50
  waitForFunction?: WaitForFunctionOptions;
50
51
  waitForSelector?: WaitForSelectorOptions;
51
- waitForTimeout?: Parameters<Page['waitForTimeout']>[0];
52
+ waitForTimeout?: number;
52
53
  }
53
54
 
54
55
  /**
@@ -190,9 +191,7 @@ export default class ContentPostRoute extends BrowserHTTPRoute {
190
191
  );
191
192
 
192
193
  if (waitForTimeout) {
193
- await page
194
- .waitForTimeout(waitForTimeout)
195
- .catch(bestAttemptCatch(bestAttempt));
194
+ await sleep(waitForTimeout).catch(bestAttemptCatch(bestAttempt));
196
195
  }
197
196
 
198
197
  if (waitForFunction) {