mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-13 17:14:02 +03:00
feat: response interception after redirects in chromium (#7910)
This commit is contained in:
parent
62a4d82b7b
commit
28fb3c776a
@ -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) {
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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>;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user