diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts index 1753ae4bcf..2183b4bb1e 100644 --- a/packages/playwright-core/src/remote/playwrightServer.ts +++ b/packages/playwright-core/src/remote/playwrightServer.ts @@ -68,7 +68,14 @@ export class PlaywrightServer { } async listen(port: number = 0): Promise { - const server = http.createServer((request, response) => { + const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { + if (request.method === 'GET' && request.url === '/json') { + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify({ + wsEndpointPath: this._options.path, + })); + return; + } response.end('Running'); }); server.on('error', error => debugLog(error)); diff --git a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts index fd210d29af..16159accc9 100644 --- a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts @@ -26,8 +26,12 @@ import { getUserAgent } from '../../common/userAgent'; import * as socks from '../../common/socksProxy'; import EventEmitter from 'events'; import { ProgressController } from '../progress'; +import type { Progress } from '../progress'; import { WebSocketTransport } from '../transport'; import { findValidator, ValidationError, type ValidatorContext } from '../../protocol/validator'; +import { fetchData } from '../../common/netUtils'; +import type { HTTPRequestParams } from '../../common/netUtils'; +import type http from 'http'; export class BrowserTypeDispatcher extends Dispatcher implements channels.BrowserTypeChannel { _type_BrowserType = true; @@ -62,7 +66,9 @@ export class BrowserTypeDispatcher extends Dispatcher { const paramsHeaders = Object.assign({ 'User-Agent': getUserAgent() }, params.headers || {}); - const transport = await WebSocketTransport.connect(progress, params.wsEndpoint, paramsHeaders, true); + const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint); + + const transport = await WebSocketTransport.connect(progress, wsEndpoint, paramsHeaders, true); let socksInterceptor: SocksInterceptor | undefined; const pipe = new JsonPipeDispatcher(this); transport.onmessage = json => { @@ -154,3 +160,34 @@ class SocksInterceptor { function tChannelForSocks(names: '*' | string[], arg: any, path: string, context: ValidatorContext) { throw new ValidationError(`${path}: channels are not expected in SocksSupport`); } + +async function urlToWSEndpoint(progress: Progress, endpointURL: string): Promise { + if (endpointURL.startsWith('ws')) + return endpointURL; + + progress.log(` retrieving websocket url from ${endpointURL}`); + const fetchUrl = new URL(endpointURL); + if (!fetchUrl.pathname.endsWith('/')) + fetchUrl.pathname += '/'; + fetchUrl.pathname += 'json'; + const json = await fetchData({ + url: fetchUrl.toString(), + method: 'GET', + timeout: progress.timeUntilDeadline(), + headers: { 'User-Agent': getUserAgent() }, + }, async (params: HTTPRequestParams, response: http.IncomingMessage) => { + return new Error(`Unexpected status ${response.statusCode} when connecting to ${fetchUrl.toString()}.\n` + + `This does not look like a Playwright server, try connecting via ws://.`); + }); + progress.throwIfAborted(); + + const wsUrl = new URL(endpointURL); + let wsEndpointPath = JSON.parse(json).wsEndpointPath; + if (wsEndpointPath.startsWith('/')) + wsEndpointPath = wsEndpointPath.substring(1); + if (!wsUrl.pathname.endsWith('/')) + wsUrl.pathname += '/'; + wsUrl.pathname += wsEndpointPath; + wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:'; + return wsUrl.toString(); +} diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 1ce9357636..70f9320424 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -645,3 +645,13 @@ test('should connect when launching', async ({ browserType, startRemoteServer, h (browserType as any)._defaultConnectOptions = undefined; }); + +test('should connect over http', async ({ browserType, startRemoteServer, mode }) => { + test.skip(mode !== 'default'); + const remoteServer = await startRemoteServer(); + + const url = new URL(remoteServer.wsEndpoint()); + const browser = await browserType.connect(`http://localhost:${url.port}`); + expect(browser.version()).toBeTruthy(); + await browser.close(); +});