feat: response interception after redirects in chromium (#7910)

This commit is contained in:
Yury Semikhatsky 2021-08-05 08:49:02 -07:00 committed by GitHub
parent 62a4d82b7b
commit 28fb3c776a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 265 additions and 149 deletions

View File

@ -178,16 +178,17 @@ export class CRNetworkManager {
if (event.request.url.startsWith('data:')) if (event.request.url.startsWith('data:'))
return; return;
if (event.responseStatusCode || event.responseErrorReason) {
if (event.responseStatusCode || event.responseHeaders || event.responseErrorReason) { const isRedirect = event.responseStatusCode && event.responseStatusCode >= 300 && event.responseStatusCode < 400;
const request = this._requestIdToRequest.get(event.networkId); const request = this._requestIdToRequest.get(event.networkId!);
if (!request || !request._onInterceptedResponse) { const route = request?._routeForRedirectChain();
if (isRedirect || !route || !route._interceptingResponse) {
this._client._sendMayFail('Fetch.continueRequest', { this._client._sendMayFail('Fetch.continueRequest', {
requestId: event.requestId requestId: event.requestId
}); });
return; return;
} }
request._onInterceptedResponse!(event); route._responseInterceptedCallback(event);
return; return;
} }
@ -204,13 +205,13 @@ export class CRNetworkManager {
_onRequest(workerFrame: frames.Frame | undefined, requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload, requestPausedEvent: Protocol.Fetch.requestPausedPayload | null) { _onRequest(workerFrame: frames.Frame | undefined, requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload, requestPausedEvent: Protocol.Fetch.requestPausedPayload | null) {
if (requestWillBeSentEvent.request.url.startsWith('data:')) if (requestWillBeSentEvent.request.url.startsWith('data:'))
return; return;
let redirectedFrom: network.Request | null = null; let redirectedFrom: InterceptableRequest | null = null;
if (requestWillBeSentEvent.redirectResponse) { if (requestWillBeSentEvent.redirectResponse) {
const request = this._requestIdToRequest.get(requestWillBeSentEvent.requestId); const request = this._requestIdToRequest.get(requestWillBeSentEvent.requestId);
// If we connect late to the target, we could have missed the requestWillBeSent event. // If we connect late to the target, we could have missed the requestWillBeSent event.
if (request) { if (request) {
this._handleRequestRedirect(request, requestWillBeSentEvent.redirectResponse, requestWillBeSentEvent.timestamp); this._handleRequestRedirect(request, requestWillBeSentEvent.redirectResponse, requestWillBeSentEvent.timestamp);
redirectedFrom = request.request; redirectedFrom = request;
} }
} }
let frame = requestWillBeSentEvent.frameId ? this._page._frameManager.frame(requestWillBeSentEvent.frameId) : workerFrame; let frame = requestWillBeSentEvent.frameId ? this._page._frameManager.frame(requestWillBeSentEvent.frameId) : workerFrame;
@ -258,26 +259,26 @@ export class CRNetworkManager {
return; return;
} }
let allowInterception = this._userRequestInterceptionEnabled; let route = null;
if (redirectedFrom) { if (requestPausedEvent) {
allowInterception = false;
// We do not support intercepting redirects. // We do not support intercepting redirects.
if (requestPausedEvent) if (redirectedFrom)
this._client._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }); this._client._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
else
route = new RouteImpl(this._client, requestPausedEvent.requestId);
} }
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document'; const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
const documentId = isNavigationRequest ? requestWillBeSentEvent.loaderId : undefined; const documentId = isNavigationRequest ? requestWillBeSentEvent.loaderId : undefined;
const request = new InterceptableRequest({ const request = new InterceptableRequest({
client: this._client,
frame, frame,
documentId, documentId,
allowInterception, route,
requestWillBeSentEvent, requestWillBeSentEvent,
requestPausedEvent, requestPausedEvent,
redirectedFrom redirectedFrom
}); });
this._requestIdToRequest.set(requestWillBeSentEvent.requestId, request); this._requestIdToRequest.set(requestWillBeSentEvent.requestId, request);
this._page._frameManager.requestStarted(request.request); this._page._frameManager.requestStarted(request.request, route || undefined);
} }
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response { _createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response {
@ -404,32 +405,32 @@ export class CRNetworkManager {
} }
} }
class InterceptableRequest implements network.RouteDelegate { class InterceptableRequest {
readonly request: network.Request; readonly request: network.Request;
_requestId: string; _requestId: string;
_interceptionId: string | null; _interceptionId: string | null;
_documentId: string | undefined; _documentId: string | undefined;
private readonly _client: CRSession;
_timestamp: number; _timestamp: number;
_wallTime: number; _wallTime: number;
_onInterceptedResponse: ((event: Protocol.Fetch.requestPausedPayload) => void) | null = null; private _route: RouteImpl | null;
private _redirectedFrom: InterceptableRequest | null;
constructor(options: { constructor(options: {
client: CRSession;
frame: frames.Frame; frame: frames.Frame;
documentId?: string; documentId?: string;
allowInterception: boolean; route: RouteImpl | null;
requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload; requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload;
requestPausedEvent: Protocol.Fetch.requestPausedPayload | null; requestPausedEvent: Protocol.Fetch.requestPausedPayload | null;
redirectedFrom: network.Request | null; redirectedFrom: InterceptableRequest | null;
}) { }) {
const { client, frame, documentId, allowInterception, requestWillBeSentEvent, requestPausedEvent, redirectedFrom } = options; const { frame, documentId, route, requestWillBeSentEvent, requestPausedEvent, redirectedFrom } = options;
this._client = client;
this._timestamp = requestWillBeSentEvent.timestamp; this._timestamp = requestWillBeSentEvent.timestamp;
this._wallTime = requestWillBeSentEvent.wallTime; this._wallTime = requestWillBeSentEvent.wallTime;
this._requestId = requestWillBeSentEvent.requestId; this._requestId = requestWillBeSentEvent.requestId;
this._interceptionId = requestPausedEvent && requestPausedEvent.requestId; this._interceptionId = requestPausedEvent && requestPausedEvent.requestId;
this._documentId = documentId; this._documentId = documentId;
this._route = route;
this._redirectedFrom = redirectedFrom;
const { const {
headers, headers,
@ -442,7 +443,28 @@ class InterceptableRequest implements network.RouteDelegate {
if (postDataEntries && postDataEntries.length && postDataEntries[0].bytes) if (postDataEntries && postDataEntries.length && postDataEntries[0].bytes)
postDataBuffer = Buffer.from(postDataEntries[0].bytes, 'base64'); postDataBuffer = Buffer.from(postDataEntries[0].bytes, 'base64');
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, url, type, method, postDataBuffer, headersObjectToArray(headers)); this.request = new network.Request(frame, redirectedFrom?.request || null, documentId, url, type, method, postDataBuffer, headersObjectToArray(headers));
}
_routeForRedirectChain(): RouteImpl | null {
let request: InterceptableRequest = this;
while (request._redirectedFrom)
request = request._redirectedFrom;
return request._route;
}
}
class RouteImpl implements network.RouteDelegate {
private readonly _client: CRSession;
private _interceptionId: string;
private _responseInterceptedPromise: Promise<Protocol.Fetch.requestPausedPayload>;
_responseInterceptedCallback: ((event: Protocol.Fetch.requestPausedPayload) => void) = () => {};
_interceptingResponse: boolean = false;
constructor(client: CRSession, interceptionId: string) {
this._client = client;
this._interceptionId = interceptionId;
this._responseInterceptedPromise = new Promise(resolve => this._responseInterceptedCallback = resolve);
} }
async responseBody(): Promise<Buffer> { async responseBody(): Promise<Buffer> {
@ -450,8 +472,8 @@ class InterceptableRequest implements network.RouteDelegate {
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
} }
async continue(overrides: types.NormalizedContinueOverrides): Promise<network.InterceptedResponse|null> { async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise<network.InterceptedResponse|null> {
const interceptPromise = overrides.interceptResponse ? new Promise<Protocol.Fetch.requestPausedPayload>(resolve => this._onInterceptedResponse = resolve) : null; this._interceptingResponse = !!overrides.interceptResponse;
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
await this._client._sendMayFail('Fetch.continueRequest', { await this._client._sendMayFail('Fetch.continueRequest', {
@ -461,10 +483,11 @@ class InterceptableRequest implements network.RouteDelegate {
method: overrides.method, method: overrides.method,
postData: overrides.postData ? overrides.postData.toString('base64') : undefined postData: overrides.postData ? overrides.postData.toString('base64') : undefined
}); });
if (!interceptPromise) if (!this._interceptingResponse)
return null; return null;
const event = await interceptPromise; const event = await this._responseInterceptedPromise;
return new network.InterceptedResponse(this.request, event.responseStatusCode!, event.responseErrorReason!, event.responseHeaders!); this._interceptionId = event.requestId;
return new network.InterceptedResponse(request, event.responseStatusCode!, event.responseErrorReason!, event.responseHeaders!);
} }
async fulfill(response: types.NormalizedFulfillResponse) { async fulfill(response: types.NormalizedFulfillResponse) {

View File

@ -60,9 +60,12 @@ export class FFNetworkManager {
return; return;
if (redirectedFrom) if (redirectedFrom)
this._requests.delete(redirectedFrom._id); this._requests.delete(redirectedFrom._id);
const request = new InterceptableRequest(this._session, frame, redirectedFrom, event); 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._requests.set(request._id, request);
this._page._frameManager.requestStarted(request.request); this._page._frameManager.requestStarted(request.request, route);
} }
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) { _onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
@ -173,34 +176,42 @@ const internalCauseToResourceType: {[key: string]: string} = {
TYPE_INTERNAL_EVENTSOURCE: 'eventsource', TYPE_INTERNAL_EVENTSOURCE: 'eventsource',
}; };
class InterceptableRequest implements network.RouteDelegate { class InterceptableRequest {
readonly request: network.Request; readonly request: network.Request;
_id: string; readonly _id: string;
private _session: FFSession;
constructor(session: FFSession, frame: frames.Frame, redirectedFrom: InterceptableRequest | null, payload: Protocol.Network.requestWillBeSentPayload) { constructor(frame: frames.Frame, redirectedFrom: InterceptableRequest | null, payload: Protocol.Network.requestWillBeSentPayload) {
this._id = payload.requestId; this._id = payload.requestId;
this._session = session;
let postDataBuffer = null; let postDataBuffer = null;
if (payload.postData) if (payload.postData)
postDataBuffer = Buffer.from(payload.postData, 'base64'); postDataBuffer = Buffer.from(payload.postData, 'base64');
this.request = new network.Request(payload.isIntercepted ? this : null, frame, redirectedFrom ? redirectedFrom.request : null, payload.navigationId, this.request = new network.Request(frame, redirectedFrom ? redirectedFrom.request : null, payload.navigationId,
payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, postDataBuffer, payload.headers); payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, postDataBuffer, payload.headers);
} }
}
class FFRouteImpl implements network.RouteDelegate {
private _request: InterceptableRequest;
private _session: FFSession;
constructor(session: FFSession, request: InterceptableRequest) {
this._session = session;
this._request = request;
}
async responseBody(forFulfill: boolean): Promise<Buffer> { async responseBody(forFulfill: boolean): Promise<Buffer> {
// Empty buffer will result in the response being used. // Empty buffer will result in the response being used.
if (forFulfill) if (forFulfill)
return Buffer.from(''); return Buffer.from('');
const response = await this._session.send('Network.getResponseBody', { const response = await this._session.send('Network.getResponseBody', {
requestId: this._id requestId: this._request._id
}); });
return Buffer.from(response.base64body, 'base64'); return Buffer.from(response.base64body, 'base64');
} }
async continue(overrides: types.NormalizedContinueOverrides): Promise<network.InterceptedResponse|null> { async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise<network.InterceptedResponse|null> {
const result = await this._session.sendMayFail('Network.resumeInterceptedRequest', { const result = await this._session.sendMayFail('Network.resumeInterceptedRequest', {
requestId: this._id, requestId: this._request._id,
url: overrides.url, url: overrides.url,
method: overrides.method, method: overrides.method,
headers: overrides.headers, headers: overrides.headers,
@ -209,14 +220,14 @@ class InterceptableRequest implements network.RouteDelegate {
}) as any; }) as any;
if (!overrides.interceptResponse) if (!overrides.interceptResponse)
return null; return null;
return new InterceptedResponse(this.request, result.response.status, result.response.statusText, result.response.headers); return new InterceptedResponse(request, result.response.status, result.response.statusText, result.response.headers);
} }
async fulfill(response: types.NormalizedFulfillResponse) { async fulfill(response: types.NormalizedFulfillResponse) {
const base64body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64'); const base64body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
await this._session.sendMayFail('Network.fulfillInterceptedRequest', { await this._session.sendMayFail('Network.fulfillInterceptedRequest', {
requestId: this._id, requestId: this._request._id,
status: response.status, status: response.status,
statusText: network.STATUS_TEXTS[String(response.status)] || '', statusText: network.STATUS_TEXTS[String(response.status)] || '',
headers: response.headers, headers: response.headers,
@ -226,7 +237,7 @@ class InterceptableRequest implements network.RouteDelegate {
async abort(errorCode: string) { async abort(errorCode: string) {
await this._session.sendMayFail('Network.abortInterceptedRequest', { await this._session.sendMayFail('Network.abortInterceptedRequest', {
requestId: this._id, requestId: this._request._id,
errorCode, errorCode,
}); });
} }

View File

@ -254,19 +254,19 @@ export class FrameManager {
frame._onLifecycleEvent(event); frame._onLifecycleEvent(event);
} }
requestStarted(request: network.Request) { requestStarted(request: network.Request, route?: network.RouteDelegate) {
const frame = request.frame(); const frame = request.frame();
this._inflightRequestStarted(request); this._inflightRequestStarted(request);
if (request._documentId) if (request._documentId)
frame.setPendingDocument({ documentId: request._documentId, request }); frame.setPendingDocument({ documentId: request._documentId, request });
if (request._isFavicon) { if (request._isFavicon) {
const route = request._route();
if (route) if (route)
route.continue(); route.continue(request, {});
return; return;
} }
this._page._browserContext.emit(BrowserContext.Events.Request, request); this._page._browserContext.emit(BrowserContext.Events.Request, request);
this._page._requestStarted(request); if (route)
this._page._requestStarted(request, route);
} }
requestReceivedResponse(response: network.Response) { requestReceivedResponse(response: network.Response) {

View File

@ -79,7 +79,6 @@ export function stripFragmentFromUrl(url: string): string {
} }
export class Request extends SdkObject { export class Request extends SdkObject {
readonly _routeDelegate: RouteDelegate | null;
private _response: Response | null = null; private _response: Response | null = null;
private _redirectedFrom: Request | null; private _redirectedFrom: Request | null;
private _redirectedTo: Request | null = null; private _redirectedTo: Request | null = null;
@ -97,12 +96,10 @@ export class Request extends SdkObject {
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {}; private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
_responseEndTiming = -1; _responseEndTiming = -1;
constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined, constructor(frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
url: string, resourceType: string, method: string, postData: Buffer | null, headers: types.HeadersArray) { url: string, resourceType: string, method: string, postData: Buffer | null, headers: types.HeadersArray) {
super(frame, 'request'); super(frame, 'request');
assert(!url.startsWith('data:'), 'Data urls should not fire requests'); assert(!url.startsWith('data:'), 'Data urls should not fire requests');
assert(!(routeDelegate && redirectedFrom), 'Should not be able to intercept redirects');
this._routeDelegate = routeDelegate;
this._frame = frame; this._frame = frame;
this._redirectedFrom = redirectedFrom; this._redirectedFrom = redirectedFrom;
if (redirectedFrom) if (redirectedFrom)
@ -185,12 +182,6 @@ export class Request extends SdkObject {
}; };
} }
_route(): Route | null {
if (!this._routeDelegate)
return null;
return new Route(this, this._routeDelegate);
}
updateWithRawHeaders(headers: types.HeadersArray) { updateWithRawHeaders(headers: types.HeadersArray) {
this._headers = headers; this._headers = headers;
this._headersMap.clear(); this._headersMap.clear();
@ -257,7 +248,7 @@ export class Route extends SdkObject {
if (oldUrl.protocol !== newUrl.protocol) if (oldUrl.protocol !== newUrl.protocol)
throw new Error('New URL must have same protocol as overridden URL'); throw new Error('New URL must have same protocol as overridden URL');
} }
this._response = await this._delegate.continue(overrides); this._response = await this._delegate.continue(this._request, overrides);
return this._response; return this._response;
} }
@ -420,7 +411,7 @@ export class InterceptedResponse extends SdkObject {
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray) { constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray) {
super(request.frame(), 'interceptedResponse'); super(request.frame(), 'interceptedResponse');
this._request = request; this._request = request._finalRequest();
this._status = status; this._status = status;
this._statusText = statusText; this._statusText = statusText;
this._headers = headers; this._headers = headers;
@ -482,7 +473,7 @@ export class WebSocket extends SdkObject {
export interface RouteDelegate { export interface RouteDelegate {
abort(errorCode: string): Promise<void>; abort(errorCode: string): Promise<void>;
fulfill(response: types.NormalizedFulfillResponse): Promise<void>; fulfill(response: types.NormalizedFulfillResponse): Promise<void>;
continue(overrides: types.NormalizedContinueOverrides): Promise<InterceptedResponse|null>; continue(request: Request, overrides: types.NormalizedContinueOverrides): Promise<InterceptedResponse|null>;
responseBody(forFulfill: boolean): Promise<Buffer>; responseBody(forFulfill: boolean): Promise<Buffer>;
} }

View File

@ -405,10 +405,8 @@ export class Page extends SdkObject {
await this._delegate.updateRequestInterception(); await this._delegate.updateRequestInterception();
} }
_requestStarted(request: network.Request) { _requestStarted(request: network.Request, routeDelegate: network.RouteDelegate) {
const route = request._route(); const route = new network.Route(request, routeDelegate);
if (!route)
return;
if (this._serverRequestInterceptor) { if (this._serverRequestInterceptor) {
this._serverRequestInterceptor(route, request); this._serverRequestInterceptor(route, request);
return; return;

View File

@ -41,95 +41,35 @@ const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = {
'failed': 'General', 'failed': 'General',
}; };
export class WKInterceptableRequest implements network.RouteDelegate { export class WKInterceptableRequest {
private readonly _session: WKSession; private readonly _session: WKSession;
readonly request: network.Request; readonly request: network.Request;
readonly _requestId: string; readonly _requestId: string;
_interceptedCallback: () => void = () => {};
private _interceptedPromise: Promise<unknown>;
_responseInterceptedCallback: ((r: Protocol.Network.Response) => void) | undefined;
private _responseInterceptedPromise: Promise<Protocol.Network.Response> | undefined;
readonly _allowInterception: boolean;
_timestamp: number; _timestamp: number;
_wallTime: number; _wallTime: number;
readonly _route: WKRouteImpl | null;
private _redirectedFrom: WKInterceptableRequest | null;
constructor(session: WKSession, allowInterception: boolean, frame: frames.Frame, event: Protocol.Network.requestWillBeSentPayload, redirectedFrom: network.Request | null, documentId: string | undefined) { constructor(session: WKSession, route: WKRouteImpl | null, frame: frames.Frame, event: Protocol.Network.requestWillBeSentPayload, redirectedFrom: WKInterceptableRequest | null, documentId: string | undefined) {
this._session = session; this._session = session;
this._requestId = event.requestId; this._requestId = event.requestId;
this._allowInterception = allowInterception; this._route = route;
const resourceType = event.type ? event.type.toLowerCase() : (redirectedFrom ? redirectedFrom.resourceType() : 'other'); this._redirectedFrom = redirectedFrom;
const resourceType = event.type ? event.type.toLowerCase() : (redirectedFrom ? redirectedFrom.request.resourceType() : 'other');
let postDataBuffer = null; let postDataBuffer = null;
this._timestamp = event.timestamp; this._timestamp = event.timestamp;
this._wallTime = event.walltime * 1000; this._wallTime = event.walltime * 1000;
if (event.request.postData) if (event.request.postData)
postDataBuffer = Buffer.from(event.request.postData, 'base64'); postDataBuffer = Buffer.from(event.request.postData, 'base64');
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, event.request.url, this.request = new network.Request(frame, redirectedFrom?.request || null, documentId, event.request.url,
resourceType, event.request.method, postDataBuffer, headersObjectToArray(event.request.headers)); resourceType, event.request.method, postDataBuffer, headersObjectToArray(event.request.headers));
this._interceptedPromise = new Promise<void>(f => this._interceptedCallback = f);
} }
async responseBody(forFulfill: boolean): Promise<Buffer> { _routeForRedirectChain(): WKRouteImpl | null {
// Empty buffer will result in the response being used. let request: WKInterceptableRequest = this;
if (forFulfill) while (request._redirectedFrom)
return Buffer.from(''); request = request._redirectedFrom;
const response = await this._session.send('Network.getInterceptedResponseBody', { requestId: this._requestId }); return request._route;
return Buffer.from(response.body, 'base64');
}
async abort(errorCode: string) {
const errorType = errorReasons[errorCode];
assert(errorType, 'Unknown error code: ' + errorCode);
await this._interceptedPromise;
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
await this._session.sendMayFail('Network.interceptRequestWithError', { requestId: this._requestId, errorType });
}
async fulfill(response: types.NormalizedFulfillResponse) {
if (300 <= response.status && response.status < 400)
throw new Error('Cannot fulfill with redirect status: ' + response.status);
await this._interceptedPromise;
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
let mimeType = response.isBase64 ? 'application/octet-stream' : 'text/plain';
const headers = headersArrayToObject(response.headers, false /* lowerCase */);
const contentType = headers['content-type'];
if (contentType)
mimeType = contentType.split(';')[0].trim();
const isResponseIntercepted = await this._responseInterceptedPromise;
await this._session.sendMayFail(isResponseIntercepted ? 'Network.interceptWithResponse' : 'Network.interceptRequestWithResponse', {
requestId: this._requestId,
status: response.status,
statusText: network.STATUS_TEXTS[String(response.status)],
mimeType,
headers,
base64Encoded: response.isBase64,
content: response.body
});
}
async continue(overrides: types.NormalizedContinueOverrides): Promise<network.InterceptedResponse|null> {
if (overrides.interceptResponse) {
await (this.request.frame()._page._delegate as WKPage)._ensureResponseInterceptionEnabled();
this._responseInterceptedPromise = new Promise(f => this._responseInterceptedCallback = f);
}
await this._interceptedPromise;
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
await this._session.sendMayFail('Network.interceptWithRequest', {
requestId: this._requestId,
url: overrides.url,
method: overrides.method,
headers: overrides.headers ? headersArrayToObject(overrides.headers, false /* lowerCase */) : undefined,
postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined
});
if (!this._responseInterceptedPromise)
return null;
const responsePayload = await this._responseInterceptedPromise;
return new InterceptedResponse(this.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers));
} }
createResponse(responsePayload: Protocol.Network.Response): network.Response { createResponse(responsePayload: Protocol.Network.Response): network.Response {
@ -152,6 +92,86 @@ export class WKInterceptableRequest implements network.RouteDelegate {
} }
} }
export class WKRouteImpl implements network.RouteDelegate {
private readonly _session: WKSession;
private readonly _requestId: string;
_requestInterceptedCallback: () => void = () => {};
private readonly _requestInterceptedPromise: Promise<unknown>;
_responseInterceptedCallback: ((responsePayload: Protocol.Network.Response) => void) | undefined;
private _responseInterceptedPromise: Promise<Protocol.Network.Response> | undefined;
private readonly _page: WKPage;
constructor(session: WKSession, page: WKPage, requestId: string) {
this._session = session;
this._page = page;
this._requestId = requestId;
this._requestInterceptedPromise = new Promise<void>(f => this._requestInterceptedCallback = f);
}
async responseBody(forFulfill: boolean): Promise<Buffer> {
// Empty buffer will result in the response being used.
if (forFulfill)
return Buffer.from('');
const response = await this._session.send('Network.getInterceptedResponseBody', { requestId: this._requestId });
return Buffer.from(response.body, 'base64');
}
async abort(errorCode: string) {
const errorType = errorReasons[errorCode];
assert(errorType, 'Unknown error code: ' + errorCode);
await this._requestInterceptedPromise;
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
await this._session.sendMayFail('Network.interceptRequestWithError', { requestId: this._requestId, errorType });
}
async fulfill(response: types.NormalizedFulfillResponse) {
if (300 <= response.status && response.status < 400)
throw new Error('Cannot fulfill with redirect status: ' + response.status);
await this._requestInterceptedPromise;
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
let mimeType = response.isBase64 ? 'application/octet-stream' : 'text/plain';
const headers = headersArrayToObject(response.headers, false /* lowerCase */);
const contentType = headers['content-type'];
if (contentType)
mimeType = contentType.split(';')[0].trim();
const isResponseIntercepted = await this._responseInterceptedPromise;
await this._session.sendMayFail(isResponseIntercepted ? 'Network.interceptWithResponse' : 'Network.interceptRequestWithResponse', {
requestId: this._requestId,
status: response.status,
statusText: network.STATUS_TEXTS[String(response.status)],
mimeType,
headers,
base64Encoded: response.isBase64,
content: response.body
});
}
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise<network.InterceptedResponse|null> {
if (overrides.interceptResponse) {
await this._page._ensureResponseInterceptionEnabled();
this._responseInterceptedPromise = new Promise(f => this._responseInterceptedCallback = f);
}
await this._requestInterceptedPromise;
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
await this._session.sendMayFail('Network.interceptWithRequest', {
requestId: this._requestId,
url: overrides.url,
method: overrides.method,
headers: overrides.headers ? headersArrayToObject(overrides.headers, false /* lowerCase */) : undefined,
postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined
});
if (!this._responseInterceptedPromise)
return null;
const responsePayload = await this._responseInterceptedPromise;
return new InterceptedResponse(request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers));
}
}
function wkMillisToRoundishMillis(value: number): number { function wkMillisToRoundishMillis(value: number): number {
// WebKit uses -1000 for unavailable. // WebKit uses -1000 for unavailable.
if (value === -1000) if (value === -1000)

View File

@ -37,7 +37,7 @@ import { WKBrowserContext } from './wkBrowser';
import { WKSession } from './wkConnection'; import { WKSession } from './wkConnection';
import { WKExecutionContext } from './wkExecutionContext'; import { WKExecutionContext } from './wkExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './wkInput'; import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './wkInput';
import { WKInterceptableRequest } from './wkInterceptableRequest'; import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest';
import { WKProvisionalPage } from './wkProvisionalPage'; import { WKProvisionalPage } from './wkProvisionalPage';
import { WKWorkers } from './wkWorkers'; import { WKWorkers } from './wkWorkers';
import { debugLogger } from '../../utils/debugLogger'; import { debugLogger } from '../../utils/debugLogger';
@ -949,16 +949,16 @@ export class WKPage implements PageDelegate {
_onRequestWillBeSent(session: WKSession, event: Protocol.Network.requestWillBeSentPayload) { _onRequestWillBeSent(session: WKSession, event: Protocol.Network.requestWillBeSentPayload) {
if (event.request.url.startsWith('data:')) if (event.request.url.startsWith('data:'))
return; return;
let redirectedFrom: network.Request | null = null; let redirectedFrom: WKInterceptableRequest | null = null;
if (event.redirectResponse) { if (event.redirectResponse) {
const request = this._requestIdToRequest.get(event.requestId); const request = this._requestIdToRequest.get(event.requestId);
// If we connect late to the target, we could have missed the requestWillBeSent event. // If we connect late to the target, we could have missed the requestWillBeSent event.
if (request) { if (request) {
this._handleRequestRedirect(request, event.redirectResponse, event.timestamp); this._handleRequestRedirect(request, event.redirectResponse, event.timestamp);
redirectedFrom = request.request; redirectedFrom = request;
} }
} }
const frame = redirectedFrom ? redirectedFrom.frame() : this._page._frameManager.frame(event.frameId); const frame = redirectedFrom ? redirectedFrom.request.frame() : this._page._frameManager.frame(event.frameId);
// sometimes we get stray network events for detached frames // sometimes we get stray network events for detached frames
// TODO(einbinder) why? // TODO(einbinder) why?
if (!frame) if (!frame)
@ -967,11 +967,13 @@ export class WKPage implements PageDelegate {
// TODO(einbinder) this will fail if we are an XHR document request // TODO(einbinder) this will fail if we are an XHR document request
const isNavigationRequest = event.type === 'Document'; const isNavigationRequest = event.type === 'Document';
const documentId = isNavigationRequest ? event.loaderId : undefined; const documentId = isNavigationRequest ? event.loaderId : undefined;
let route = null;
// We do not support intercepting redirects. // We do not support intercepting redirects.
const allowInterception = this._page._needsRequestInterception() && !redirectedFrom; if (this._page._needsRequestInterception() && !redirectedFrom)
const request = new WKInterceptableRequest(session, allowInterception, frame, event, redirectedFrom, documentId); route = new WKRouteImpl(session, this, event.requestId);
const request = new WKInterceptableRequest(session, route, frame, event, redirectedFrom, documentId);
this._requestIdToRequest.set(event.requestId, request); this._requestIdToRequest.set(event.requestId, request);
this._page._frameManager.requestStarted(request.request); this._page._frameManager.requestStarted(request.request, route || undefined);
} }
private _handleRequestRedirect(request: WKInterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number) { private _handleRequestRedirect(request: WKInterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number) {
@ -990,22 +992,23 @@ export class WKPage implements PageDelegate {
session.sendMayFail('Network.interceptRequestWithError', {errorType: 'Cancellation', requestId: event.requestId}); session.sendMayFail('Network.interceptRequestWithError', {errorType: 'Cancellation', requestId: event.requestId});
return; return;
} }
if (!request._allowInterception) { if (!request._route) {
// Intercepted, although we do not intend to allow interception. // Intercepted, although we do not intend to allow interception.
// Just continue. // Just continue.
session.sendMayFail('Network.interceptWithRequest', { requestId: request._requestId }); session.sendMayFail('Network.interceptWithRequest', { requestId: request._requestId });
} else { } else {
request._interceptedCallback(); request._route._requestInterceptedCallback();
} }
} }
_onResponseIntercepted(session: WKSession, event: Protocol.Network.responseInterceptedPayload) { _onResponseIntercepted(session: WKSession, event: Protocol.Network.responseInterceptedPayload) {
const request = this._requestIdToRequest.get(event.requestId); const request = this._requestIdToRequest.get(event.requestId);
if (!request || !request._responseInterceptedCallback) { const route = request?._routeForRedirectChain();
if (!route?._responseInterceptedCallback) {
session.sendMayFail('Network.interceptContinue', { requestId: event.requestId, stage: 'response' }); session.sendMayFail('Network.interceptContinue', { requestId: event.requestId, stage: 'response' });
return; return;
} }
request._responseInterceptedCallback(event.response); route._responseInterceptedCallback(event.response);
} }
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) { _onResponseReceived(event: Protocol.Network.responseReceivedPayload) {

View File

@ -150,8 +150,8 @@ it('should be abortable after interception', async ({page, server, browserName})
expect(failed).toBe(true); expect(failed).toBe(true);
}); });
it('should fulfill after redirects', async ({page, server}) => { it('should fulfill after redirects', async ({page, server, browserName}) => {
it.fixme(); it.fixme(browserName !== 'chromium');
server.setRedirect('/redirect/1.html', '/redirect/2.html'); server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/empty.html'); server.setRedirect('/redirect/2.html', '/empty.html');
const expectedUrls = ['/redirect/1.html', '/redirect/2.html', '/empty.html'].map(s => server.PREFIX + s); const expectedUrls = ['/redirect/1.html', '/redirect/2.html', '/empty.html'].map(s => server.PREFIX + s);
@ -161,7 +161,9 @@ it('should fulfill after redirects', async ({page, server}) => {
page.on('request', request => requestUrls.push(request.url())); page.on('request', request => requestUrls.push(request.url()));
page.on('response', response => responseUrls.push(response.url())); page.on('response', response => responseUrls.push(response.url()));
page.on('requestfinished', request => requestFinishedUrls.push(request.url())); page.on('requestfinished', request => requestFinishedUrls.push(request.url()));
let routeCalls = 0;
await page.route('**/*', async route => { await page.route('**/*', async route => {
++routeCalls;
// @ts-expect-error // @ts-expect-error
await route._intercept({}); await route._intercept({});
await route.fulfill({ await route.fulfill({
@ -178,6 +180,7 @@ it('should fulfill after redirects', async ({page, server}) => {
expect(responseUrls).toEqual(expectedUrls); expect(responseUrls).toEqual(expectedUrls);
await response.finished(); await response.finished();
expect(requestFinishedUrls).toEqual(expectedUrls); expect(requestFinishedUrls).toEqual(expectedUrls);
expect(routeCalls).toBe(1);
const redirectChain = []; const redirectChain = [];
for (let req = response.request(); req; req = req.redirectedFrom()) for (let req = response.request(); req; req = req.redirectedFrom())
@ -189,3 +192,70 @@ it('should fulfill after redirects', async ({page, server}) => {
expect(response.headers()['content-type']).toBe('text/plain'); expect(response.headers()['content-type']).toBe('text/plain');
expect(await response.text()).toBe('Yo, page!'); expect(await response.text()).toBe('Yo, page!');
}); });
it('should fulfill original response after redirects', async ({page, browserName, server}) => {
it.fixme(browserName !== 'chromium');
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/title.html');
const expectedUrls = ['/redirect/1.html', '/redirect/2.html', '/title.html'].map(s => server.PREFIX + s);
const requestUrls = [];
const responseUrls = [];
const requestFinishedUrls = [];
page.on('request', request => requestUrls.push(request.url()));
page.on('response', response => responseUrls.push(response.url()));
page.on('requestfinished', request => requestFinishedUrls.push(request.url()));
let routeCalls = 0;
await page.route('**/*', async route => {
++routeCalls;
// @ts-expect-error
await route._intercept({});
await route.fulfill();
});
const response = await page.goto(server.PREFIX + '/redirect/1.html');
expect(requestUrls).toEqual(expectedUrls);
expect(responseUrls).toEqual(expectedUrls);
await response.finished();
expect(requestFinishedUrls).toEqual(expectedUrls);
expect(routeCalls).toBe(1);
const redirectChain = [];
for (let req = response.request(); req; req = req.redirectedFrom())
redirectChain.unshift(req.url());
expect(redirectChain).toEqual(expectedUrls);
expect(response.status()).toBe(200);
expect(await response.text()).toBe('<title>Woof-Woof</title>\n');
});
it('should abort after redirects', async ({page, browserName, server}) => {
it.fixme(browserName !== 'chromium');
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/title.html');
const expectedUrls = ['/redirect/1.html', '/redirect/2.html', '/title.html'].map(s => server.PREFIX + s);
const requestUrls = [];
const responseUrls = [];
const requestFinishedUrls = [];
const requestFailedUrls = [];
page.on('request', request => requestUrls.push(request.url()));
page.on('response', response => responseUrls.push(response.url()));
page.on('requestfinished', request => requestFinishedUrls.push(request.url()));
page.on('requestfailed', request => requestFailedUrls.push(request.url()));
let routeCalls = 0;
await page.route('**/*', async route => {
++routeCalls;
// @ts-expect-error
await route._intercept({});
await route.abort('connectionreset');
});
try {
await page.goto(server.PREFIX + '/redirect/1.html');
} catch (e) {
expect(e.message).toContain('ERR_CONNECTION_RESET');
}
expect(requestUrls).toEqual(expectedUrls);
expect(responseUrls).toEqual(expectedUrls.slice(0, -1));
expect(requestFinishedUrls).toEqual(expectedUrls.slice(0, -1));
expect(requestFailedUrls).toEqual(expectedUrls.slice(-1));
expect(routeCalls).toBe(1);
});