diff --git a/docs/src/api/class-browsertype.md b/docs/src/api/class-browsertype.md index a848477350..ab7f940177 100644 --- a/docs/src/api/class-browsertype.md +++ b/docs/src/api/class-browsertype.md @@ -75,6 +75,7 @@ This methods attaches Playwright to an existing browser instance. * langs: js - `params` <[Object]> - `wsEndpoint` <[string]> A browser websocket endpoint to connect to. + - `headers` <[Object]<[string], [string]>> Additional HTTP headers to be sent with web socket connect request. Optional. - `slowMo` <[float]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0. - `logger` <[Logger]> Logger sink for Playwright logging. Optional. @@ -87,6 +88,12 @@ This methods attaches Playwright to an existing browser instance. A browser websocket endpoint to connect to. +### param: BrowserType.connect.headers +* langs: java, python +- `headers` <[string]> + +Additional HTTP headers to be sent with web socket connect request. Optional. + ### option: BrowserType.connect.slowMo * langs: java, python - `slowMo` <[float]> @@ -117,6 +124,7 @@ Connecting over the Chrome DevTools Protocol is only supported for Chromium-base * langs: js - `params` <[Object]> - `endpointURL` <[string]> A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`. + - `headers` <[Object]<[string], [string]>> Additional HTTP headers to be sent with connect request. Optional. - `slowMo` <[float]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0. - `logger` <[Logger]> Logger sink for Playwright logging. Optional. @@ -129,6 +137,12 @@ Connecting over the Chrome DevTools Protocol is only supported for Chromium-base A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`. +### option: BrowserType.connectOverCDP.headers +* langs: java, python +- `headers` <[Object]<[string], [string]>> + +Additional HTTP headers to be sent with connect request. Optional. + ### option: BrowserType.connectOverCDP.slowMo * langs: java, python - `slowMo` <[float]> diff --git a/src/client/browserType.ts b/src/client/browserType.ts index 4242d22fdd..74cf140ed1 100644 --- a/src/client/browserType.ts +++ b/src/client/browserType.ts @@ -26,7 +26,7 @@ import { Events } from './events'; import { TimeoutSettings } from '../utils/timeoutSettings'; import { ChildProcess } from 'child_process'; import { envObjectToArray } from './clientHelper'; -import { assert, makeWaitForNextTask } from '../utils/utils'; +import { assert, headersObjectToArray, makeWaitForNextTask } from '../utils/utils'; import { kBrowserClosedError } from '../utils/errors'; import * as api from '../../types/types'; import type { Playwright } from './playwright'; @@ -117,6 +117,7 @@ export class BrowserType extends ChannelOwner { - waitForNextTask(() => connection.dispatch(JSON.parse(event.data))); + waitForNextTask(() => { + try { + connection.dispatch(JSON.parse(event.data)); + } catch (e) { + ws.close(); + } + }); }); - return await new Promise(async (fulfill, reject) => { if ((params as any).__testHookBeforeCreateBrowser) { try { @@ -193,9 +199,11 @@ export class BrowserType extends ChannelOwner { + const headers = params.headers ? headersObjectToArray(params.headers) : undefined; const result = await channel.connectOverCDP({ sdkLanguage: 'javascript', endpointURL: 'endpointURL' in params ? params.endpointURL : params.wsEndpoint, + headers, slowMo: params.slowMo, timeout: params.timeout }); diff --git a/src/client/types.ts b/src/client/types.ts index f0276cb4c8..a91ef287c1 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -70,6 +70,7 @@ export type LaunchPersistentContextOptions = Omit Validator): Scheme { scheme.BrowserTypeConnectOverCDPParams = tObject({ sdkLanguage: tString, endpointURL: tString, + headers: tOptional(tArray(tType('NameValue'))), slowMo: tOptional(tNumber), timeout: tOptional(tNumber), }); diff --git a/src/server/chromium/chromium.ts b/src/server/chromium/chromium.ts index 5e4ba34b0d..3f516200bd 100644 --- a/src/server/chromium/chromium.ts +++ b/src/server/chromium/chromium.ts @@ -25,7 +25,7 @@ import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../tra import { CRDevTools } from './crDevTools'; import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import * as types from '../types'; -import { debugMode } from '../../utils/utils'; +import { debugMode, headersArrayToObject } from '../../utils/utils'; import { RecentLogsCollector } from '../../utils/debugLogger'; import { ProgressController } from '../progress'; import { TimeoutSettings } from '../../utils/timeoutSettings'; @@ -50,12 +50,15 @@ export class Chromium extends BrowserType { return super.executablePath(channel); } - async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, sdkLanguage: string }, timeout?: number) { + async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, sdkLanguage: string, headers?: types.HeadersArray }, timeout?: number) { const controller = new ProgressController(metadata, this); controller.setLogName('browser'); const browserLogsCollector = new RecentLogsCollector(); return controller.run(async progress => { - const chromeTransport = await WebSocketTransport.connect(progress, await urlToWSEndpoint(endpointURL)); + let headersMap: { [key: string]: string; } | undefined; + if (options.headers) + headersMap = headersArrayToObject(options.headers, false); + const chromeTransport = await WebSocketTransport.connect(progress, await urlToWSEndpoint(endpointURL), headersMap); const browserProcess: BrowserProcess = { close: async () => { await chromeTransport.closeAndWait(); diff --git a/src/server/transport.ts b/src/server/transport.ts index bef9cd1136..8dbcc8f916 100644 --- a/src/server/transport.ts +++ b/src/server/transport.ts @@ -52,9 +52,9 @@ export class WebSocketTransport implements ConnectionTransport { onclose?: () => void; readonly wsEndpoint: string; - static async connect(progress: Progress, url: string): Promise { + static async connect(progress: Progress, url: string, headers?: { [key: string]: string; }): Promise { progress.log(` ${url}`); - const transport = new WebSocketTransport(progress, url); + const transport = new WebSocketTransport(progress, url, headers); let success = false; progress.cleanupWhenAborted(async () => { if (!success) @@ -75,12 +75,13 @@ export class WebSocketTransport implements ConnectionTransport { return transport; } - constructor(progress: Progress, url: string) { + constructor(progress: Progress, url: string, headers?: { [key: string]: string; }) { this.wsEndpoint = url; this._ws = new WebSocket(url, [], { perMessageDeflate: false, maxPayload: 256 * 1024 * 1024, // 256Mb, handshakeTimeout: progress.timeUntilDeadline(), + headers }); this._progress = progress; // The 'ws' module in node sometimes sends us multiple messages in a single task. @@ -91,8 +92,12 @@ export class WebSocketTransport implements ConnectionTransport { this._ws.addEventListener('message', event => { messageWrap(() => { - if (this.onmessage) - this.onmessage.call(null, JSON.parse(event.data)); + try { + if (this.onmessage) + this.onmessage.call(null, JSON.parse(event.data)); + } catch (e) { + this._ws.close(); + } }); }); diff --git a/tests/browsertype-connect.spec.ts b/tests/browsertype-connect.spec.ts index c9fa81d7ce..76ea5df0a4 100644 --- a/tests/browsertype-connect.spec.ts +++ b/tests/browsertype-connect.spec.ts @@ -60,6 +60,21 @@ test('should be able to connect two browsers at the same time', async ({browserT await browser2.close(); }); +test('should send extra headers with connect request', async ({browserType, startRemoteServer, server}) => { + const [request] = await Promise.all([ + server.waitForWebSocketConnectionRequest(), + browserType.connect({ + wsEndpoint: `ws://localhost:${server.PORT}/ws`, + headers: { + 'User-Agent': 'Playwright', + 'foo': 'bar', + } + }).catch(() => {}) + ]); + expect(request.headers['user-agent']).toBe('Playwright'); + expect(request.headers['foo']).toBe('bar'); +}); + test('disconnected event should be emitted when browser is closed or server is closed', async ({browserType, startRemoteServer}) => { const remoteServer = await startRemoteServer(); diff --git a/tests/chromium/chromium.spec.ts b/tests/chromium/chromium.spec.ts index f3f4bb44af..42c297280d 100644 --- a/tests/chromium/chromium.spec.ts +++ b/tests/chromium/chromium.spec.ts @@ -219,4 +219,34 @@ playwrightTest.describe('chromium', () => { await browserServer.close(); } }); + playwrightTest('should send extra headers with connect request', async ({browserType, browserOptions, server}, testInfo) => { + { + const [request] = await Promise.all([ + server.waitForWebSocketConnectionRequest(), + browserType.connectOverCDP({ + wsEndpoint: `ws://localhost:${server.PORT}/ws`, + headers: { + 'User-Agent': 'Playwright', + 'foo': 'bar', + } + }).catch(() => {}) + ]); + expect(request.headers['user-agent']).toBe('Playwright'); + expect(request.headers['foo']).toBe('bar'); + } + { + const [request] = await Promise.all([ + server.waitForWebSocketConnectionRequest(), + browserType.connectOverCDP({ + endpointURL: `ws://localhost:${server.PORT}/ws`, + headers: { + 'User-Agent': 'Playwright', + 'foo': 'bar', + } + }).catch(() => {}) + ]); + expect(request.headers['user-agent']).toBe('Playwright'); + expect(request.headers['foo']).toBe('bar'); + } + }); }); diff --git a/types/types.d.ts b/types/types.d.ts index cbef10196d..d2bf5876c7 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -10877,6 +10877,11 @@ export interface ConnectOverCDPOptions { */ endpointURL: string; + /** + * Additional HTTP headers to be sent with connect request. Optional. + */ + headers?: { [key: string]: string; }; + /** * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. * Defaults to 0. @@ -10901,6 +10906,11 @@ export interface ConnectOptions { */ wsEndpoint: string; + /** + * Additional HTTP headers to be sent with web socket connect request. Optional. + */ + headers?: { [key: string]: string; }; + /** * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. * Defaults to 0. diff --git a/utils/testserver/index.d.ts b/utils/testserver/index.d.ts index fc7ffee9fd..56779ae93e 100644 --- a/utils/testserver/index.d.ts +++ b/utils/testserver/index.d.ts @@ -28,6 +28,7 @@ export class TestServer { setRoute(path: string, handler: (message: IncomingMessage & {postBody: Buffer}, response: ServerResponse) => void); setRedirect(from: string, to: string); waitForRequest(path: string): Promise; + waitForWebSocketConnectionRequest(): Promise; reset(); serveFile(request: IncomingMessage, response: ServerResponse); serveFile(request: IncomingMessage, response: ServerResponse, filePath: string); diff --git a/utils/testserver/index.js b/utils/testserver/index.js index dfe67cce1a..d7e9b0e6fb 100644 --- a/utils/testserver/index.js +++ b/utils/testserver/index.js @@ -293,6 +293,12 @@ class TestServer { } } + waitForWebSocketConnectionRequest() { + return new Promise(fullfil => { + this._wsServer.once('connection', (ws, req) => fullfil(req)); + }); + } + _onWebSocketConnection(ws) { ws.send('incoming'); }