@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.
- package/bin/scaffold/tsconfig.json +1 -1
- package/build/browsers/index.d.ts +1 -10
- package/build/browsers/index.js +19 -29
- 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/build/utils.d.ts +1 -0
- package/build/utils.js +13 -0
- package/package.json +3 -3
- package/src/browsers/index.ts +24 -45
- 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/src/utils.ts +26 -0
- package/static/docs/swagger.json +70 -98
- package/static/function/client.js +192 -488
|
@@ -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');
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Browserless,
|
|
3
|
+
BrowserlessSessionJSON,
|
|
3
4
|
Config,
|
|
4
5
|
Metrics,
|
|
5
6
|
exists,
|
|
7
|
+
fetchJson,
|
|
6
8
|
sleep,
|
|
7
9
|
} from '@browserless.io/browserless';
|
|
8
10
|
import { chromium } from 'playwright-core';
|
|
@@ -85,6 +87,70 @@ describe('WebSocket API', function () {
|
|
|
85
87
|
await Promise.all([browser.disconnect(), browserTwo.disconnect()]);
|
|
86
88
|
});
|
|
87
89
|
|
|
90
|
+
it('does not close browsers when multiple clients are connected', async () => {
|
|
91
|
+
const config = new Config();
|
|
92
|
+
config.setToken('browserless');
|
|
93
|
+
const metrics = new Metrics();
|
|
94
|
+
await start({ config, metrics });
|
|
95
|
+
|
|
96
|
+
// Single session
|
|
97
|
+
const browser = await puppeteer.connect({
|
|
98
|
+
browserWSEndpoint: `ws://localhost:3000?token=browserless`,
|
|
99
|
+
});
|
|
100
|
+
const [session] = (await fetchJson(
|
|
101
|
+
'http://localhost:3000/sessions?token=browserless',
|
|
102
|
+
)) as BrowserlessSessionJSON[];
|
|
103
|
+
expect(session.numbConnected).to.equal(1);
|
|
104
|
+
|
|
105
|
+
// Two sessions
|
|
106
|
+
const browserTwo = await puppeteer.connect({
|
|
107
|
+
browserWSEndpoint: `ws://localhost:3000/devtools/browser/${session.browserId}?token=browserless`,
|
|
108
|
+
});
|
|
109
|
+
const [twoSessions] = (await fetchJson(
|
|
110
|
+
'http://localhost:3000/sessions?token=browserless',
|
|
111
|
+
)) as BrowserlessSessionJSON[];
|
|
112
|
+
expect(twoSessions.numbConnected).to.equal(2);
|
|
113
|
+
|
|
114
|
+
// Back to a single session
|
|
115
|
+
await browser.disconnect();
|
|
116
|
+
await sleep(50);
|
|
117
|
+
const [oneSession] = (await fetchJson(
|
|
118
|
+
'http://localhost:3000/sessions?token=browserless',
|
|
119
|
+
)) as BrowserlessSessionJSON[];
|
|
120
|
+
expect(oneSession.numbConnected).to.equal(1);
|
|
121
|
+
|
|
122
|
+
// No sessions connected
|
|
123
|
+
await browserTwo.disconnect();
|
|
124
|
+
await sleep(50);
|
|
125
|
+
const sessionsFinal = (await fetchJson(
|
|
126
|
+
'http://localhost:3000/sessions?token=browserless',
|
|
127
|
+
)) as BrowserlessSessionJSON[];
|
|
128
|
+
expect(sessionsFinal).to.have.length(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('disconnects all clients when the timeout is reached', async () => {
|
|
132
|
+
const config = new Config();
|
|
133
|
+
config.setToken('browserless');
|
|
134
|
+
config.setTimeout(1000);
|
|
135
|
+
config.setConcurrent(2);
|
|
136
|
+
const metrics = new Metrics();
|
|
137
|
+
await start({ config, metrics });
|
|
138
|
+
const browser = await puppeteer.connect({
|
|
139
|
+
browserWSEndpoint: `ws://localhost:3000?token=browserless`,
|
|
140
|
+
});
|
|
141
|
+
const [session] = (await fetchJson(
|
|
142
|
+
'http://localhost:3000/sessions?token=browserless',
|
|
143
|
+
)) as BrowserlessSessionJSON[];
|
|
144
|
+
const browserTwo = await puppeteer.connect({
|
|
145
|
+
browserWSEndpoint: `ws://localhost:3000/devtools/browser/${session.browserId}?token=browserless`,
|
|
146
|
+
});
|
|
147
|
+
await sleep(3000);
|
|
148
|
+
expect(metrics.get().successful).to.equal(0);
|
|
149
|
+
expect(metrics.get().timedout).to.equal(2);
|
|
150
|
+
expect(browser.connected).to.be.false;
|
|
151
|
+
expect(browserTwo.connected).to.be.false;
|
|
152
|
+
});
|
|
153
|
+
|
|
88
154
|
it('rejects websocket requests', async () => {
|
|
89
155
|
const config = new Config();
|
|
90
156
|
config.setToken('browserless');
|
|
@@ -261,13 +327,13 @@ describe('WebSocket API', function () {
|
|
|
261
327
|
await start({ config, metrics });
|
|
262
328
|
|
|
263
329
|
const browser = await puppeteer.connect({
|
|
264
|
-
browserWSEndpoint: `ws://localhost:3000?timeout=
|
|
330
|
+
browserWSEndpoint: `ws://localhost:3000?timeout=1000&token=browserless`,
|
|
265
331
|
});
|
|
266
|
-
|
|
267
|
-
await sleep(
|
|
268
|
-
browser.disconnect();
|
|
332
|
+
expect(browser.connected).to.be.true;
|
|
333
|
+
await sleep(1200);
|
|
269
334
|
expect(metrics.get().timedout).to.equal(1);
|
|
270
335
|
expect(metrics.get().successful).to.equal(0);
|
|
336
|
+
expect(browser.connected).to.be.false;
|
|
271
337
|
});
|
|
272
338
|
|
|
273
339
|
it('allows the file-chooser', async () =>
|
|
@@ -16,7 +16,7 @@ export interface QuerySchema extends SystemQueryParameters {
|
|
|
16
16
|
export default class CDPExistingBrowser extends BrowserWebsocketRoute {
|
|
17
17
|
auth = true;
|
|
18
18
|
browser = CDPChromium;
|
|
19
|
-
concurrency =
|
|
19
|
+
concurrency = true;
|
|
20
20
|
description = `Connect to an already-running Chromium with a library like puppeteer, or others, that work over chrome-devtools-protocol.`;
|
|
21
21
|
path = WebsocketRoutes.browser;
|
|
22
22
|
tags = [APITags.browserWS];
|
package/src/types.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -255,6 +255,32 @@ export const removeNullStringify = (
|
|
|
255
255
|
export const jsonOrString = (maybeJson: string): unknown | string =>
|
|
256
256
|
safeParse(maybeJson) ?? maybeJson;
|
|
257
257
|
|
|
258
|
+
export const generateDataDir = async (
|
|
259
|
+
sessionId: string = id(),
|
|
260
|
+
config: Config,
|
|
261
|
+
): Promise<string> => {
|
|
262
|
+
const baseDirectory = await config.getDataDir();
|
|
263
|
+
const dataDirPath = path.join(
|
|
264
|
+
baseDirectory,
|
|
265
|
+
`browserless-data-dir-${sessionId}`,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (await exists(dataDirPath)) {
|
|
269
|
+
debug(`Data directory already exists, not creating "${dataDirPath}"`);
|
|
270
|
+
return dataDirPath;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
debug(`Generating user-data-dir at ${dataDirPath}`);
|
|
274
|
+
|
|
275
|
+
await fs.mkdir(dataDirPath, { recursive: true }).catch((err) => {
|
|
276
|
+
throw new ServerError(
|
|
277
|
+
`Error creating data-directory "${dataDirPath}": ${err}`,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return dataDirPath;
|
|
282
|
+
};
|
|
283
|
+
|
|
258
284
|
export const readBody = async (
|
|
259
285
|
req: Request,
|
|
260
286
|
): Promise<ReturnType<typeof safeParse>> => {
|