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

View File

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

View File

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

View File

@ -79,7 +79,6 @@ export function stripFragmentFromUrl(url: string): string {
}
export class Request extends SdkObject {
readonly _routeDelegate: RouteDelegate | null;
private _response: Response | null = null;
private _redirectedFrom: Request | null;
private _redirectedTo: Request | null = null;
@ -97,12 +96,10 @@ export class Request extends SdkObject {
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
_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) {
super(frame, 'request');
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._redirectedFrom = 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) {
this._headers = headers;
this._headersMap.clear();
@ -257,7 +248,7 @@ export class Route extends SdkObject {
if (oldUrl.protocol !== newUrl.protocol)
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;
}
@ -420,7 +411,7 @@ export class InterceptedResponse extends SdkObject {
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray) {
super(request.frame(), 'interceptedResponse');
this._request = request;
this._request = request._finalRequest();
this._status = status;
this._statusText = statusText;
this._headers = headers;
@ -482,7 +473,7 @@ export class WebSocket extends SdkObject {
export interface RouteDelegate {
abort(errorCode: string): 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>;
}

View File

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

View File

@ -41,95 +41,35 @@ const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = {
'failed': 'General',
};
export class WKInterceptableRequest implements network.RouteDelegate {
export class WKInterceptableRequest {
private readonly _session: WKSession;
readonly request: network.Request;
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;
_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._requestId = event.requestId;
this._allowInterception = allowInterception;
const resourceType = event.type ? event.type.toLowerCase() : (redirectedFrom ? redirectedFrom.resourceType() : 'other');
this._route = route;
this._redirectedFrom = redirectedFrom;
const resourceType = event.type ? event.type.toLowerCase() : (redirectedFrom ? redirectedFrom.request.resourceType() : 'other');
let postDataBuffer = null;
this._timestamp = event.timestamp;
this._wallTime = event.walltime * 1000;
if (event.request.postData)
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));
this._interceptedPromise = new Promise<void>(f => this._interceptedCallback = 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._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));
_routeForRedirectChain(): WKRouteImpl | null {
let request: WKInterceptableRequest = this;
while (request._redirectedFrom)
request = request._redirectedFrom;
return request._route;
}
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 {
// WebKit uses -1000 for unavailable.
if (value === -1000)

View File

@ -37,7 +37,7 @@ import { WKBrowserContext } from './wkBrowser';
import { WKSession } from './wkConnection';
import { WKExecutionContext } from './wkExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './wkInput';
import { WKInterceptableRequest } from './wkInterceptableRequest';
import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest';
import { WKProvisionalPage } from './wkProvisionalPage';
import { WKWorkers } from './wkWorkers';
import { debugLogger } from '../../utils/debugLogger';
@ -949,16 +949,16 @@ export class WKPage implements PageDelegate {
_onRequestWillBeSent(session: WKSession, event: Protocol.Network.requestWillBeSentPayload) {
if (event.request.url.startsWith('data:'))
return;
let redirectedFrom: network.Request | null = null;
let redirectedFrom: WKInterceptableRequest | null = null;
if (event.redirectResponse) {
const request = this._requestIdToRequest.get(event.requestId);
// If we connect late to the target, we could have missed the requestWillBeSent event.
if (request) {
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
// TODO(einbinder) why?
if (!frame)
@ -967,11 +967,13 @@ export class WKPage implements PageDelegate {
// TODO(einbinder) this will fail if we are an XHR document request
const isNavigationRequest = event.type === 'Document';
const documentId = isNavigationRequest ? event.loaderId : undefined;
let route = null;
// We do not support intercepting redirects.
const allowInterception = this._page._needsRequestInterception() && !redirectedFrom;
const request = new WKInterceptableRequest(session, allowInterception, frame, event, redirectedFrom, documentId);
if (this._page._needsRequestInterception() && !redirectedFrom)
route = new WKRouteImpl(session, this, event.requestId);
const request = new WKInterceptableRequest(session, route, frame, event, redirectedFrom, documentId);
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) {
@ -990,22 +992,23 @@ export class WKPage implements PageDelegate {
session.sendMayFail('Network.interceptRequestWithError', {errorType: 'Cancellation', requestId: event.requestId});
return;
}
if (!request._allowInterception) {
if (!request._route) {
// Intercepted, although we do not intend to allow interception.
// Just continue.
session.sendMayFail('Network.interceptWithRequest', { requestId: request._requestId });
} else {
request._interceptedCallback();
request._route._requestInterceptedCallback();
}
}
_onResponseIntercepted(session: WKSession, event: Protocol.Network.responseInterceptedPayload) {
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' });
return;
}
request._responseInterceptedCallback(event.response);
route._responseInterceptedCallback(event.response);
}
_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);
});
it('should fulfill after redirects', async ({page, server}) => {
it.fixme();
it('should fulfill after redirects', async ({page, server, browserName}) => {
it.fixme(browserName !== 'chromium');
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/empty.html');
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('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({
@ -178,6 +180,7 @@ it('should fulfill after redirects', async ({page, server}) => {
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())
@ -189,3 +192,70 @@ it('should fulfill after redirects', async ({page, server}) => {
expect(response.headers()['content-type']).toBe('text/plain');
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);
});