Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ export class CRNetworkManager {
if (this._page) {
sessionInfo.eventListeners.push(...[
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!.frameManager.onWebSocketCreated(e.requestId, e.url)),
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!.frameManager.onWebSocketRequest(e.requestId)),
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!.frameManager.onWebSocketRequest(e.requestId, headersObjectToArray(e.request.headers, '\n'), e.wallTime, e.timestamp)),
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText, headersObjectToArray(e.response.headers, '\n'))),
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page!.frameManager.webSocketClosed(e.requestId)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page!.frameManager.webSocketError(e.requestId, e.errorMessage)),
]);
Expand Down
37 changes: 27 additions & 10 deletions packages/playwright-core/src/server/firefox/ffNetworkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,21 @@ import { eventsHelper } from '@utils/eventsHelper';
import * as network from '../network';

import type { FFSession } from './ffConnection';
import type { FFPage } from './ffPage';
import type { HeadersArray } from '../../server/types';
import type { RegisteredListener } from '@utils/eventsHelper';
import type * as frames from '../frames';
import type { Page } from '../page';
import type * as types from '../types';
import type { Protocol } from './protocol';

export class FFNetworkManager {
private _session: FFSession;
private _requests: Map<string, InterceptableRequest>;
private _page: Page;
private _page: FFPage;
private _eventListeners: RegisteredListener[];
private _webSocketRequestIds = new Set<string>();

constructor(session: FFSession, page: Page) {
constructor(session: FFSession, page: FFPage) {
this._session = session;

this._requests = new Map();
Expand All @@ -59,26 +60,36 @@ export class FFNetworkManager {

_onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) {
const redirectedFrom = event.redirectedFrom ? (this._requests.get(event.redirectedFrom) || null) : null;
const frame = redirectedFrom ? redirectedFrom.request.frame() : (event.frameId ? this._page.frameManager.frame(event.frameId) : null);
const frame = redirectedFrom ? redirectedFrom.request.frame() : (event.frameId ? this._page._page.frameManager.frame(event.frameId) : null);
if (!frame)
return;
// Align with Chromium and WebKit and not expose preflight OPTIONS requests to the client.
if (event.method === 'OPTIONS' && !event.isIntercepted)
return;
if (redirectedFrom)
this._requests.delete(redirectedFrom._id);
// Align with Chromium and WebKit by having WebSocket be handled separately from other network activity.
if (event.cause === 'TYPE_WEBSOCKET') {
this._webSocketRequestIds.add(event.requestId);
this._page._onWebSocketRequestWillBeSent(event.requestId, event.url, event.headers);
return;
}
const request = new InterceptableRequest(frame, redirectedFrom, event);
let route;
if (event.isIntercepted)
route = new FFRouteImpl(this._session, request);
this._requests.set(request._id, request);
this._page.frameManager.requestStarted(request.request, route);
this._page._page.frameManager.requestStarted(request.request, route);
}

_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
const request = this._requests.get(event.requestId);
if (!request)
if (!request) {
// Align with Chromium and WebKit by having WebSocket be handled separately from other network activity.
if (this._webSocketRequestIds.has(event.requestId))
this._page._onWebSocketResponseReceived(event.requestId, event.status, event.statusText, event.headers);
return;
}
const getResponseBody = async () => {
const response = await this._session.send('Network.getResponseBody', {
requestId: request._id
Expand Down Expand Up @@ -124,13 +135,19 @@ export class FFNetworkManager {
response.setRawResponseHeaders(null);
// Headers size are not available in Firefox.
response.setResponseHeadersSize(null);
this._page.frameManager.requestReceivedResponse(response);
this._page._page.frameManager.requestReceivedResponse(response);
}

_onRequestFinished(event: Protocol.Network.requestFinishedPayload) {
const request = this._requests.get(event.requestId);
if (!request)
if (!request) {
// Align with Chromium and WebKit by having WebSocket be handled separately from other network activity.
if (this._webSocketRequestIds.has(event.requestId)) {
this._webSocketRequestIds.delete(event.requestId);
this._page._onWebSocketRequestFinished(event.requestId);
}
return;
}
const response = request.request._existingResponse()!;
response.setTransferSize(event.transferSize);
response.setEncodedBodySize(event.encodedBodySize);
Expand All @@ -145,7 +162,7 @@ export class FFNetworkManager {
response._requestFinished(responseEndTime);
}
response._setHttpVersion(event.protocolVersion ?? null);
this._page.frameManager.reportRequestFinished(request.request, response);
this._page._page.frameManager.reportRequestFinished(request.request, response);
}

_onRequestFailed(event: Protocol.Network.requestFailedPayload) {
Expand All @@ -161,7 +178,7 @@ export class FFNetworkManager {
response._setHttpVersion(null);
}
request.request._setFailureText(event.errorCode);
this._page.frameManager.requestFailed(request.request, event.errorCode === 'NS_BINDING_ABORTED');
this._page._page.frameManager.requestFailed(request.request, event.errorCode === 'NS_BINDING_ABORTED');
}
}

Expand Down
57 changes: 53 additions & 4 deletions packages/playwright-core/src/server/firefox/ffPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
* limitations under the License.
*/

import { assert } from '@isomorphic/assert';
import { splitErrorMessage } from '@isomorphic/stackTrace';
import { eventsHelper } from '@utils/eventsHelper';
import * as dialog from '../dialog';
import * as dom from '../dom';
import * as network from '../network';
import { InitScript } from '../page';
import { Page, Worker } from '../page';
import { FFSession } from './ffConnection';
Expand Down Expand Up @@ -53,6 +55,8 @@ export class FFPage implements PageDelegate {
private _eventListeners: RegisteredListener[];
private _workers = new Map<string, { frameId: string, session: FFSession }>();
private _initScripts: { initScript: InitScript, worldName?: string }[] = [];
private _webSocketRequests = new Map<string, { url: string, headers: types.HeadersArray }>();
private _webSocketResponses = new Map<string, { status: number, statusText: string, headers: types.HeadersArray }>();

constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
this._session = session;
Expand All @@ -64,7 +68,7 @@ export class FFPage implements PageDelegate {
this._browserContext = browserContext;
this._page = new Page(this, browserContext);
this.rawMouse.setPage(this._page);
this._networkManager = new FFNetworkManager(session, this._page);
this._networkManager = new FFNetworkManager(session, this);
this._page.on(Page.Events.FrameDetached, frame => this._removeContextsForFrame(frame));
// TODO: remove Page.willOpenNewWindowAsynchronously from the protocol.
this._eventListeners = [
Expand All @@ -90,6 +94,7 @@ export class FFPage implements PageDelegate {
eventsHelper.addEventListener(this._session, 'Page.crashed', this._onCrashed.bind(this)),

eventsHelper.addEventListener(this._session, 'Page.webSocketCreated', this._onWebSocketCreated.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.webSocketOpened', this._onWebSocketOpened.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.webSocketClosed', this._onWebSocketClosed.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.webSocketFrameReceived', this._onWebSocketFrameReceived.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.webSocketFrameSent', this._onWebSocketFrameSent.bind(this)),
Expand Down Expand Up @@ -119,7 +124,51 @@ export class FFPage implements PageDelegate {

_onWebSocketCreated(event: Protocol.Page.webSocketCreatedPayload) {
this._page.frameManager.onWebSocketCreated(webSocketId(event.frameId, event.wsid), event.requestURL);
this._page.frameManager.onWebSocketRequest(webSocketId(event.frameId, event.wsid));
}

_onWebSocketRequestWillBeSent(requestId: string, url: string, headers: types.HeadersArray) {
this._webSocketRequests.set(requestId, { url, headers });
}

_onWebSocketResponseReceived(requestId: string, status: number, statusText: string, headers: types.HeadersArray) {
this._webSocketResponses.set(requestId, { status, statusText, headers });
}

_onWebSocketRequestFinished(requestId: string) {
const response = this._webSocketResponses.get(requestId);
assert(response);
// If the request does not succeed then the WebSocket will never open, so pretend that it did.
if (response.status >= 400) {
const request = this._webSocketRequests.get(requestId);
assert(request);

this._webSocketRequests.delete(requestId);
this._webSocketResponses.delete(requestId);

const url = network.parseURL(request.url);
assert(url);
url.protocol = url.protocol === 'https' ? 'wss' : 'ws';

this._page.frameManager.onWebSocketCreated(requestId, url.toString());
this._page.frameManager.onWebSocketRequest(requestId, request.headers);
this._page.frameManager.onWebSocketResponse(requestId, response.status, response.statusText, response.headers);
this._page.frameManager.webSocketClosed(requestId);
return;
}
}

_onWebSocketOpened(event: Protocol.Page.webSocketOpenedPayload) {
const request = this._webSocketRequests.get(event.requestId);
assert(request);

const response = this._webSocketResponses.get(event.requestId);
assert(response);

this._webSocketRequests.delete(event.requestId);
this._webSocketResponses.delete(event.requestId);

this._page.frameManager.onWebSocketRequest(webSocketId(event.frameId, event.wsid), request.headers);
this._page.frameManager.onWebSocketResponse(webSocketId(event.frameId, event.wsid), response.status, response.statusText, response.headers);
}

_onWebSocketClosed(event: Protocol.Page.webSocketClosedPayload) {
Expand All @@ -129,11 +178,11 @@ export class FFPage implements PageDelegate {
}

_onWebSocketFrameReceived(event: Protocol.Page.webSocketFrameReceivedPayload) {
this._page.frameManager.webSocketFrameReceived(webSocketId(event.frameId, event.wsid), event.opcode, event.data);
this._page.frameManager.webSocketFrameReceived(webSocketId(event.frameId, event.wsid), event.opcode, event.data, event.timestamp);
}

_onWebSocketFrameSent(event: Protocol.Page.webSocketFrameSentPayload) {
this._page.frameManager.onWebSocketFrameSent(webSocketId(event.frameId, event.wsid), event.opcode, event.data);
this._page.frameManager.onWebSocketFrameSent(webSocketId(event.frameId, event.wsid), event.opcode, event.data, event.timestamp);
}

_onExecutionContextCreated(payload: Protocol.Runtime.executionContextCreatedPayload) {
Expand Down
35 changes: 24 additions & 11 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,43 +403,56 @@ export class FrameManager {
this._webSockets.set(requestId, ws);
}

onWebSocketRequest(requestId: string) {
onWebSocketRequest(requestId: string, headers: types.HeadersArray, wallTime?: number, timestamp?: number) {
const ws = this._webSockets.get(requestId);
if (ws && ws.markAsNotified())
if (!ws)
return;

if (ws.markAsNotified())
this._page.emit(Page.Events.WebSocket, ws);

ws.requestSent(headers, wallTime, timestamp);
}

onWebSocketResponse(requestId: string, status: number, statusText: string) {
onWebSocketResponse(requestId: string, status: number, statusText: string, headers: types.HeadersArray) {
const ws = this._webSockets.get(requestId);
if (status < 400)
if (!ws)
return;
if (ws)

ws.responseReceived(status, statusText, headers);
if (status >= 400)
ws.error(`${statusText}: ${status}`);
}

onWebSocketFrameSent(requestId: string, opcode: number, data: string) {
onWebSocketFrameSent(requestId: string, opcode: number, data: string, timestamp: number) {
const ws = this._webSockets.get(requestId);
if (ws)
ws.frameSent(opcode, data);
ws.frameSent(opcode, data, timestamp);
}

webSocketFrameReceived(requestId: string, opcode: number, data: string) {
webSocketFrameReceived(requestId: string, opcode: number, data: string, timestamp: number) {
const ws = this._webSockets.get(requestId);
if (ws)
ws.frameReceived(opcode, data);
ws.frameReceived(opcode, data, timestamp);
}

webSocketClosed(requestId: string) {
const ws = this._webSockets.get(requestId);
if (ws)
if (ws) {
if (ws.markAsNotified())
this._page.emit(Page.Events.WebSocket, ws);
ws.closed();
}
this._webSockets.delete(requestId);
}

webSocketError(requestId: string, errorMessage: string): void {
const ws = this._webSockets.get(requestId);
if (ws)
if (ws) {
if (ws.markAsNotified())
this._page.emit(Page.Events.WebSocket, ws);
ws.error(errorMessage);
}
}

private _fireInternalFrameNavigation(frame: Frame, event: NavigationEvent) {
Expand Down
63 changes: 60 additions & 3 deletions packages/playwright-core/src/server/har/harTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import { helper } from '../helper';
import * as network from '../network';
import { nullProgress } from '../progress';

import { Page } from '../page';

import type { RegisteredListener } from '@utils/eventsHelper';
import type { APIRequestEvent, APIRequestFinishedEvent } from '../fetch';
import type { Page } from '../page';
import type { Worker } from '../page';
import type { HeadersArray, LifecycleEvent } from '../types';
import type * as har from '@trace/har';
Expand Down Expand Up @@ -102,7 +103,10 @@ export class HarTracer {
];
if (this._context instanceof BrowserContext) {
this._eventListeners.push(
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._createPageEntryIfNeeded(page)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => {
this._addPageEventListeners(page);
this._createPageEntryIfNeeded(page);
}),
eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})),
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFailed, request => this._onRequestFailed(request)),
Expand All @@ -111,11 +115,21 @@ export class HarTracer {
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFulfilled, request => this._onRequestFulfilled(request)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestContinued, request => this._onRequestContinued(request)),
);
for (const page of this._context.pages())
for (const page of this._context.pages()) {
this._addPageEventListeners(page);
this._createPageEntryIfNeeded(page);
}
}
}

private _addPageEventListeners(page: Page) {
if (this._page && page !== this._page)
Comment thread
yury-s marked this conversation as resolved.
return;
this._eventListeners.push(
eventsHelper.addEventListener(page, Page.Events.WebSocket, (webSocket: network.WebSocket) => this._onWebSocket(page, webSocket)),
);
}

private _shouldIncludeEntryWithUrl(urlString: string) {
return !this._options.urlFilter || urlMatches(this._baseURL, urlString, this._options.urlFilter);
}
Expand Down Expand Up @@ -418,6 +432,49 @@ export class HarTracer {
harEntry._wasContinued = true;
}

private _onWebSocket(page: Page, webSocket: network.WebSocket) {
if (!this._shouldIncludeEntryWithUrl(webSocket.url()))
return;
const url = network.parseURL(webSocket.url());
if (!url)
return;

const pageEntry = this._createPageEntryIfNeeded(page);
const harEntry = createHarEntry(pageEntry?.id, 'GET', url, page.mainFrame().guid, this._options);
harEntry._resourceType = 'websocket';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we populate it for other types from Request.resourceType and maybe place to harEntry.request._resourceType (to match playwright api) or is it not worth it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can do that in a followup

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

harEntry._webSocketMessages = [];

const eventListeners = [
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Request, ({ headers }: { headers: HeadersArray }) => {
this._recordRequestHeadersAndCookies(harEntry, headers);
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Response, ({ status, statusText, headers }: { status: number, statusText: string, headers: HeadersArray }) => {
harEntry.response.status = status;
harEntry.response.statusText = statusText;
this._recordResponseHeaders(harEntry, headers);
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.FrameSent, ({ opcode, data, timestamp }: { opcode: number, data: string, timestamp: number }) => {
harEntry._webSocketMessages!.push({ type: 'send', time: timestamp, opcode, data });
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.FrameReceived, ({ opcode, data, timestamp }: { opcode: number, data: string, timestamp: number }) => {
harEntry._webSocketMessages!.push({ type: 'receive', time: timestamp, opcode, data });
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.SocketError, (errorMessage: string) => {
harEntry.response._failureText = errorMessage;
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Close, () => {
eventsHelper.removeEventListeners(eventListeners);

if (this._started)
this._delegate.onEntryFinished(harEntry);
}),
];
this._eventListeners.push(...eventListeners);

if (this._started)
this._delegate.onEntryStarted(harEntry);
}

private _storeResponseContent(buffer: Buffer | undefined, content: har.Content, resourceType: string) {
if (!buffer) {
content.size = 0;
Expand Down
Loading
Loading