@browserless.io/browserless 2.2.0-beta-2 → 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.
- package/build/browsers/index.js +9 -3
- package/build/router.js +13 -6
- package/build/routes/chromium/http/content-post.body.json +15 -19
- package/build/routes/chromium/http/content-post.d.ts +1 -1
- package/build/routes/chromium/http/content-post.js +2 -4
- package/build/routes/chromium/http/pdf-post.body.json +15 -19
- package/build/routes/chromium/http/pdf-post.d.ts +1 -1
- package/build/routes/chromium/http/pdf-post.js +10 -6
- package/build/routes/chromium/http/scrape-post.body.json +15 -19
- package/build/routes/chromium/http/scrape-post.d.ts +3 -3
- package/build/routes/chromium/http/scrape-post.js +2 -4
- package/build/routes/chromium/http/scrape-post.response.json +22 -38
- package/build/routes/chromium/http/screenshot-post.body.json +15 -19
- package/build/routes/chromium/http/screenshot-post.d.ts +1 -1
- package/build/routes/chromium/http/screenshot-post.js +2 -4
- package/build/routes/chromium/tests/content.spec.js +27 -1
- package/build/routes/chromium/tests/websocket.spec.js +53 -4
- package/build/routes/chromium/ws/browser.js +1 -1
- package/build/routes/management/http/sessions-get.response.json +4 -0
- package/build/types.d.ts +1 -0
- package/package.json +3 -3
- package/src/browsers/index.ts +11 -4
- package/src/router.ts +13 -7
- package/src/routes/chromium/http/content-post.ts +3 -4
- package/src/routes/chromium/http/pdf-post.ts +13 -6
- package/src/routes/chromium/http/scrape-post.ts +5 -6
- package/src/routes/chromium/http/screenshot-post.ts +3 -4
- package/src/routes/chromium/tests/content.spec.ts +28 -1
- package/src/routes/chromium/tests/websocket.spec.ts +70 -4
- package/src/routes/chromium/ws/browser.ts +1 -1
- package/src/types.ts +1 -0
- package/static/docs/swagger.json +70 -98
- package/static/function/client.js +192 -488
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"cookies": {
|
|
24
24
|
"type": "array",
|
|
25
25
|
"items": {
|
|
26
|
-
"$ref": "#/definitions/
|
|
26
|
+
"$ref": "#/definitions/CookieParam"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"emulateMediaType": {
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
"username"
|
|
239
239
|
]
|
|
240
240
|
},
|
|
241
|
-
"
|
|
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
|
|
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
|
|
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@
|
|
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@
|
|
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@
|
|
529
|
+
"__@toStringTag@96609": {
|
|
534
530
|
"type": "string"
|
|
535
531
|
}
|
|
536
532
|
},
|
|
537
533
|
"additionalProperties": false,
|
|
538
534
|
"required": [
|
|
539
|
-
"__@toStringTag@
|
|
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@
|
|
545
|
+
"__@species@96710": {
|
|
550
546
|
"$ref": "#/definitions/SharedArrayBuffer"
|
|
551
547
|
},
|
|
552
|
-
"__@toStringTag@
|
|
548
|
+
"__@toStringTag@96609": {
|
|
553
549
|
"type": "string",
|
|
554
550
|
"const": "SharedArrayBuffer"
|
|
555
551
|
}
|
|
556
552
|
},
|
|
557
553
|
"additionalProperties": false,
|
|
558
554
|
"required": [
|
|
559
|
-
"__@species@
|
|
560
|
-
"__@toStringTag@
|
|
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?:
|
|
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
|
|
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=
|
|
240
|
+
browserWSEndpoint: `ws://localhost:3000?timeout=1000&token=browserless`,
|
|
193
241
|
});
|
|
194
|
-
|
|
195
|
-
|
|
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 =
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserless.io/browserless",
|
|
3
|
-
"version": "2.2.0-beta-
|
|
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": "^
|
|
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.
|
|
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",
|
package/src/browsers/index.ts
CHANGED
|
@@ -125,7 +125,7 @@ export class BrowserManager {
|
|
|
125
125
|
{
|
|
126
126
|
...session,
|
|
127
127
|
browser: browser.constructor.name,
|
|
128
|
-
browserId: browser.wsEndpoint()?.split('/').pop(),
|
|
128
|
+
browserId: browser.wsEndpoint()?.split('/').pop() as string,
|
|
129
129
|
initialConnectURL: new URL(session.initialConnectURL, serverAddress)
|
|
130
130
|
.href,
|
|
131
131
|
killURL: session.id
|
|
@@ -170,6 +170,11 @@ export class BrowserManager {
|
|
|
170
170
|
const cleanupACtions: Array<() => Promise<void>> = [];
|
|
171
171
|
this.debug(`${session.numbConnected} Client(s) are currently connected`);
|
|
172
172
|
|
|
173
|
+
// Don't close if there's clients still connected
|
|
174
|
+
if (session.numbConnected > 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
173
178
|
this.debug(`Closing browser session`);
|
|
174
179
|
cleanupACtions.push(() => browser.close());
|
|
175
180
|
|
|
@@ -234,13 +239,15 @@ export class BrowserManager {
|
|
|
234
239
|
if (req.parsed.pathname.includes('/devtools/browser')) {
|
|
235
240
|
const sessions = Array.from(this.browsers);
|
|
236
241
|
const id = req.parsed.pathname.split('/').pop() as string;
|
|
237
|
-
const
|
|
242
|
+
const found = sessions.find(([b]) =>
|
|
238
243
|
b.wsEndpoint()?.includes(req.parsed.pathname),
|
|
239
244
|
);
|
|
240
245
|
|
|
241
|
-
if (
|
|
246
|
+
if (found) {
|
|
247
|
+
const [browser, session] = found;
|
|
248
|
+
++session.numbConnected;
|
|
242
249
|
this.debug(`Located browser with ID ${id}`);
|
|
243
|
-
return browser
|
|
250
|
+
return browser;
|
|
244
251
|
}
|
|
245
252
|
|
|
246
253
|
throw new NotFound(
|
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
|
|
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?:
|
|
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
|
|
194
|
-
.waitForTimeout(waitForTimeout)
|
|
195
|
-
.catch(bestAttemptCatch(bestAttempt));
|
|
194
|
+
await sleep(waitForTimeout).catch(bestAttemptCatch(bestAttempt));
|
|
196
195
|
}
|
|
197
196
|
|
|
198
197
|
if (waitForFunction) {
|
|
@@ -21,12 +21,14 @@ import {
|
|
|
21
21
|
rejectRequestPattern,
|
|
22
22
|
rejectResourceTypes,
|
|
23
23
|
requestInterceptors,
|
|
24
|
+
sleep,
|
|
24
25
|
waitForEvent as waitForEvt,
|
|
25
26
|
waitForFunction as waitForFn,
|
|
26
27
|
writeResponse,
|
|
27
28
|
} from '@browserless.io/browserless';
|
|
28
29
|
import { Page } from 'puppeteer-core';
|
|
29
30
|
import { ServerResponse } from 'http';
|
|
31
|
+
import { Stream } from 'stream';
|
|
30
32
|
|
|
31
33
|
export interface BodySchema {
|
|
32
34
|
addScriptTag?: Array<Parameters<Page['addScriptTag']>[0]>;
|
|
@@ -50,7 +52,7 @@ export interface BodySchema {
|
|
|
50
52
|
waitForEvent?: WaitForEventOptions;
|
|
51
53
|
waitForFunction?: WaitForFunctionOptions;
|
|
52
54
|
waitForSelector?: WaitForSelectorOptions;
|
|
53
|
-
waitForTimeout?:
|
|
55
|
+
waitForTimeout?: number;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface QuerySchema extends SystemQueryParameters {
|
|
@@ -111,9 +113,10 @@ export default class PDFPost extends BrowserHTTPRoute {
|
|
|
111
113
|
setJavaScriptEnabled,
|
|
112
114
|
userAgent,
|
|
113
115
|
viewport,
|
|
116
|
+
waitForEvent,
|
|
114
117
|
waitForFunction,
|
|
115
118
|
waitForSelector,
|
|
116
|
-
|
|
119
|
+
waitForTimeout,
|
|
117
120
|
bestAttempt = false,
|
|
118
121
|
} = req.body as BodySchema;
|
|
119
122
|
|
|
@@ -196,6 +199,10 @@ export default class PDFPost extends BrowserHTTPRoute {
|
|
|
196
199
|
bestAttemptCatch(bestAttempt),
|
|
197
200
|
);
|
|
198
201
|
|
|
202
|
+
if (waitForTimeout) {
|
|
203
|
+
await sleep(waitForTimeout).catch(bestAttemptCatch(bestAttempt));
|
|
204
|
+
}
|
|
205
|
+
|
|
199
206
|
if (waitForFunction) {
|
|
200
207
|
await waitForFn(page, waitForFunction).catch(
|
|
201
208
|
bestAttemptCatch(bestAttempt),
|
|
@@ -227,11 +234,11 @@ export default class PDFPost extends BrowserHTTPRoute {
|
|
|
227
234
|
}
|
|
228
235
|
}
|
|
229
236
|
|
|
230
|
-
const
|
|
237
|
+
const pdfBuffer = await page.pdf(options);
|
|
238
|
+
const readStream = new Stream.PassThrough();
|
|
239
|
+
readStream.end(pdfBuffer);
|
|
231
240
|
|
|
232
|
-
await new Promise((
|
|
233
|
-
return pdfStream.pipe(res).once('finish', resolve).once('error', reject);
|
|
234
|
-
});
|
|
241
|
+
await new Promise((r) => readStream.pipe(res).once('close', r));
|
|
235
242
|
|
|
236
243
|
page.close().catch(noop);
|
|
237
244
|
};
|
|
@@ -28,10 +28,11 @@ import {
|
|
|
28
28
|
rejectRequestPattern,
|
|
29
29
|
rejectResourceTypes,
|
|
30
30
|
requestInterceptors,
|
|
31
|
+
sleep,
|
|
31
32
|
waitForEvent as waitForEvt,
|
|
32
33
|
waitForFunction as waitForFn,
|
|
33
34
|
} from '@browserless.io/browserless';
|
|
34
|
-
import {
|
|
35
|
+
import { Cookie, Page } from 'puppeteer-core';
|
|
35
36
|
import { ServerResponse } from 'http';
|
|
36
37
|
|
|
37
38
|
export interface BodySchema {
|
|
@@ -56,7 +57,7 @@ export interface BodySchema {
|
|
|
56
57
|
waitForEvent?: WaitForEventOptions;
|
|
57
58
|
waitForFunction?: WaitForFunctionOptions;
|
|
58
59
|
waitForSelector?: WaitForSelectorOptions;
|
|
59
|
-
waitForTimeout?:
|
|
60
|
+
waitForTimeout?: number;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export type QuerySchema = SystemQueryParameters & {
|
|
@@ -135,7 +136,7 @@ export interface ResponseSchema {
|
|
|
135
136
|
/**
|
|
136
137
|
* List of cookies for the site or null
|
|
137
138
|
*/
|
|
138
|
-
cookies:
|
|
139
|
+
cookies: Cookie[] | null;
|
|
139
140
|
|
|
140
141
|
/**
|
|
141
142
|
* The HTML string of the website or null
|
|
@@ -361,9 +362,7 @@ export default class ScrapePost extends BrowserHTTPRoute {
|
|
|
361
362
|
}
|
|
362
363
|
|
|
363
364
|
if (waitForTimeout) {
|
|
364
|
-
await
|
|
365
|
-
.waitForTimeout(waitForTimeout)
|
|
366
|
-
.catch(bestAttemptCatch(bestAttempt));
|
|
365
|
+
await sleep(waitForTimeout).catch(bestAttemptCatch(bestAttempt));
|
|
367
366
|
}
|
|
368
367
|
|
|
369
368
|
if (waitForFunction) {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
rejectResourceTypes,
|
|
23
23
|
requestInterceptors,
|
|
24
24
|
scrollThroughPage,
|
|
25
|
+
sleep,
|
|
25
26
|
waitForEvent as waitForEvt,
|
|
26
27
|
waitForFunction as waitForFn,
|
|
27
28
|
} from '@browserless.io/browserless';
|
|
@@ -62,7 +63,7 @@ export interface BodySchema {
|
|
|
62
63
|
waitForEvent?: WaitForEventOptions;
|
|
63
64
|
waitForFunction?: WaitForFunctionOptions;
|
|
64
65
|
waitForSelector?: WaitForSelectorOptions;
|
|
65
|
-
waitForTimeout?:
|
|
66
|
+
waitForTimeout?: number;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export default class ScreenshotPost extends BrowserHTTPRoute {
|
|
@@ -205,9 +206,7 @@ export default class ScreenshotPost extends BrowserHTTPRoute {
|
|
|
205
206
|
);
|
|
206
207
|
|
|
207
208
|
if (waitForTimeout) {
|
|
208
|
-
await
|
|
209
|
-
.waitForTimeout(waitForTimeout)
|
|
210
|
-
.catch(bestAttemptCatch(bestAttempt));
|
|
209
|
+
await sleep(waitForTimeout).catch(bestAttemptCatch(bestAttempt));
|
|
211
210
|
}
|
|
212
211
|
|
|
213
212
|
if (waitForFunction) {
|
|
@@ -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
|
|
|
4
4
|
describe('/content API', function () {
|
|
@@ -43,6 +43,33 @@ describe('/content API', function () {
|
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
it('cancels request when they are closed early', async () => {
|
|
47
|
+
const config = new Config();
|
|
48
|
+
const metrics = new Metrics();
|
|
49
|
+
await start({ config, metrics });
|
|
50
|
+
const body = {
|
|
51
|
+
url: 'https://cnn.com',
|
|
52
|
+
};
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const signal = controller.signal;
|
|
55
|
+
const promise = fetch('http://localhost:3000/content', {
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
headers: {
|
|
58
|
+
'content-type': 'application/json',
|
|
59
|
+
},
|
|
60
|
+
method: 'POST',
|
|
61
|
+
signal,
|
|
62
|
+
}).catch(async (error) => {
|
|
63
|
+
await sleep(100);
|
|
64
|
+
expect(error).to.have.property('name', 'AbortError');
|
|
65
|
+
expect(metrics.get().error).to.equal(1);
|
|
66
|
+
expect(metrics.get().successful).to.equal(0);
|
|
67
|
+
});
|
|
68
|
+
await sleep(1000);
|
|
69
|
+
controller.abort();
|
|
70
|
+
return promise;
|
|
71
|
+
});
|
|
72
|
+
|
|
46
73
|
it('404s GET requests', async () => {
|
|
47
74
|
const config = new Config();
|
|
48
75
|
config.setToken('browserless');
|