mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(cookies): read response headers off extra info event, if any (#8526)
This commit is contained in:
parent
65dc238b32
commit
e47bacdecb
@ -28,7 +28,7 @@ import { Events } from './events';
|
|||||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
import { TimeoutSettings } from '../utils/timeoutSettings';
|
||||||
import { Waiter } from './waiter';
|
import { Waiter } from './waiter';
|
||||||
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
|
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
|
||||||
import { isUnderTest, headersObjectToArray, mkdirIfNeeded, isString } from '../utils/utils';
|
import { isUnderTest, headersObjectToArray, mkdirIfNeeded, isString, headersArrayToObject } from '../utils/utils';
|
||||||
import { isSafeCloseError } from '../utils/errors';
|
import { isSafeCloseError } from '../utils/errors';
|
||||||
import * as api from '../../types/types';
|
import * as api from '../../types/types';
|
||||||
import * as structs from '../../types/structs';
|
import * as structs from '../../types/structs';
|
||||||
@ -86,8 +86,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
|||||||
});
|
});
|
||||||
this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page)));
|
this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page)));
|
||||||
this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page)));
|
this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page)));
|
||||||
this._channel.on('requestFinished', ({ request, responseEndTiming, page, requestSizes }) =>
|
this._channel.on('requestFinished', ({ request, response, responseEndTiming, responseHeaders, page, requestSizes }) =>
|
||||||
this._onRequestFinished(network.Request.from(request), responseEndTiming, requestSizes, Page.fromNullable(page))
|
this._onRequestFinished(network.Request.from(request), network.Response.fromNullable(response), responseEndTiming, responseHeaders, requestSizes, Page.fromNullable(page))
|
||||||
);
|
);
|
||||||
this._channel.on('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page)));
|
this._channel.on('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page)));
|
||||||
this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f));
|
this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f));
|
||||||
@ -126,10 +126,12 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
|||||||
page.emit(Events.Page.RequestFailed, request);
|
page.emit(Events.Page.RequestFailed, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onRequestFinished(request: network.Request, responseEndTiming: number, requestSizes: channels.RequestSizes, page: Page | null) {
|
private _onRequestFinished(request: network.Request, response: network.Response | null, responseEndTiming: number, responseHeaders: channels.NameValue[] | undefined, requestSizes: channels.RequestSizes, page: Page | null) {
|
||||||
if (request._timing)
|
if (request._timing)
|
||||||
request._timing.responseEnd = responseEndTiming;
|
request._timing.responseEnd = responseEndTiming;
|
||||||
request._sizes = requestSizes;
|
request._sizes = requestSizes;
|
||||||
|
if (response && responseHeaders)
|
||||||
|
response._headers = headersArrayToObject(responseHeaders, true /* lowerCase */);
|
||||||
this.emit(Events.BrowserContext.RequestFinished, request);
|
this.emit(Events.BrowserContext.RequestFinished, request);
|
||||||
if (page)
|
if (page)
|
||||||
page.emit(Events.Page.RequestFinished, request);
|
page.emit(Events.Page.RequestFinished, request);
|
||||||
|
@ -382,7 +382,7 @@ export type RequestSizes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class Response extends ChannelOwner<channels.ResponseChannel, channels.ResponseInitializer> implements api.Response {
|
export class Response extends ChannelOwner<channels.ResponseChannel, channels.ResponseInitializer> implements api.Response {
|
||||||
private _headers: Headers;
|
_headers: Headers;
|
||||||
private _request: Request;
|
private _request: Request;
|
||||||
|
|
||||||
static from(response: channels.ResponseChannel): Response {
|
static from(response: channels.ResponseChannel): Response {
|
||||||
|
@ -83,8 +83,10 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||||||
responseEndTiming: request._responseEndTiming,
|
responseEndTiming: request._responseEndTiming,
|
||||||
page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined())
|
page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined())
|
||||||
}));
|
}));
|
||||||
context.on(BrowserContext.Events.RequestFinished, (request: Request) => this._dispatchEvent('requestFinished', {
|
context.on(BrowserContext.Events.RequestFinished, ({ request, response}: { request: Request, response: Response | null }) => this._dispatchEvent('requestFinished', {
|
||||||
request: RequestDispatcher.from(scope, request),
|
request: RequestDispatcher.from(scope, request),
|
||||||
|
response: ResponseDispatcher.fromNullable(scope, response),
|
||||||
|
responseHeaders: response?.headers(),
|
||||||
responseEndTiming: request._responseEndTiming,
|
responseEndTiming: request._responseEndTiming,
|
||||||
requestSizes: request.sizes(),
|
requestSizes: request.sizes(),
|
||||||
page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined()),
|
page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined()),
|
||||||
|
@ -146,10 +146,7 @@ export type InterceptedResponse = {
|
|||||||
request: RequestChannel,
|
request: RequestChannel,
|
||||||
status: number,
|
status: number,
|
||||||
statusText: string,
|
statusText: string,
|
||||||
headers: {
|
headers: NameValue[],
|
||||||
name: string,
|
|
||||||
value: string,
|
|
||||||
}[],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FetchResponse = {
|
export type FetchResponse = {
|
||||||
@ -787,7 +784,9 @@ export type BrowserContextRequestFailedEvent = {
|
|||||||
};
|
};
|
||||||
export type BrowserContextRequestFinishedEvent = {
|
export type BrowserContextRequestFinishedEvent = {
|
||||||
request: RequestChannel,
|
request: RequestChannel,
|
||||||
|
response?: ResponseChannel,
|
||||||
responseEndTiming: number,
|
responseEndTiming: number,
|
||||||
|
responseHeaders?: NameValue[],
|
||||||
requestSizes: RequestSizes,
|
requestSizes: RequestSizes,
|
||||||
page?: PageChannel,
|
page?: PageChannel,
|
||||||
};
|
};
|
||||||
@ -2577,10 +2576,7 @@ export type RequestInitializer = {
|
|||||||
resourceType: string,
|
resourceType: string,
|
||||||
method: string,
|
method: string,
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
headers: {
|
headers: NameValue[],
|
||||||
name: string,
|
|
||||||
value: string,
|
|
||||||
}[],
|
|
||||||
isNavigationRequest: boolean,
|
isNavigationRequest: boolean,
|
||||||
redirectedFrom?: RequestChannel,
|
redirectedFrom?: RequestChannel,
|
||||||
};
|
};
|
||||||
@ -2671,14 +2667,8 @@ export type ResponseInitializer = {
|
|||||||
url: string,
|
url: string,
|
||||||
status: number,
|
status: number,
|
||||||
statusText: string,
|
statusText: string,
|
||||||
requestHeaders: {
|
requestHeaders: NameValue[],
|
||||||
name: string,
|
headers: NameValue[],
|
||||||
value: string,
|
|
||||||
}[],
|
|
||||||
headers: {
|
|
||||||
name: string,
|
|
||||||
value: string,
|
|
||||||
}[],
|
|
||||||
timing: ResourceTiming,
|
timing: ResourceTiming,
|
||||||
};
|
};
|
||||||
export interface ResponseChannel extends Channel {
|
export interface ResponseChannel extends Channel {
|
||||||
|
@ -213,11 +213,7 @@ InterceptedResponse:
|
|||||||
statusText: string
|
statusText: string
|
||||||
headers:
|
headers:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items: NameValue
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name: string
|
|
||||||
value: string
|
|
||||||
|
|
||||||
|
|
||||||
FetchResponse:
|
FetchResponse:
|
||||||
@ -755,7 +751,11 @@ BrowserContext:
|
|||||||
requestFinished:
|
requestFinished:
|
||||||
parameters:
|
parameters:
|
||||||
request: Request
|
request: Request
|
||||||
|
response: Response?
|
||||||
responseEndTiming: number
|
responseEndTiming: number
|
||||||
|
responseHeaders:
|
||||||
|
type: array?
|
||||||
|
items: NameValue
|
||||||
requestSizes: RequestSizes
|
requestSizes: RequestSizes
|
||||||
page: Page?
|
page: Page?
|
||||||
|
|
||||||
@ -2112,11 +2112,7 @@ Request:
|
|||||||
postData: binary?
|
postData: binary?
|
||||||
headers:
|
headers:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items: NameValue
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name: string
|
|
||||||
value: string
|
|
||||||
isNavigationRequest: boolean
|
isNavigationRequest: boolean
|
||||||
redirectedFrom: Request?
|
redirectedFrom: Request?
|
||||||
|
|
||||||
@ -2190,18 +2186,10 @@ Response:
|
|||||||
statusText: string
|
statusText: string
|
||||||
requestHeaders:
|
requestHeaders:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items: NameValue
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name: string
|
|
||||||
value: string
|
|
||||||
headers:
|
headers:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items: NameValue
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name: string
|
|
||||||
value: string
|
|
||||||
timing: ResourceTiming
|
timing: ResourceTiming
|
||||||
|
|
||||||
|
|
||||||
|
@ -144,10 +144,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||||||
request: tChannel('Request'),
|
request: tChannel('Request'),
|
||||||
status: tNumber,
|
status: tNumber,
|
||||||
statusText: tString,
|
statusText: tString,
|
||||||
headers: tArray(tObject({
|
headers: tArray(tType('NameValue')),
|
||||||
name: tString,
|
|
||||||
value: tString,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
scheme.FetchResponse = tObject({
|
scheme.FetchResponse = tObject({
|
||||||
url: tString,
|
url: tString,
|
||||||
|
@ -39,6 +39,7 @@ export class CRNetworkManager {
|
|||||||
private _requestIdToRequestPausedEvent = new Map<string, Protocol.Fetch.requestPausedPayload>();
|
private _requestIdToRequestPausedEvent = new Map<string, Protocol.Fetch.requestPausedPayload>();
|
||||||
private _eventListeners: RegisteredListener[];
|
private _eventListeners: RegisteredListener[];
|
||||||
private _requestIdToExtraInfo = new Map<string, Protocol.Network.requestWillBeSentExtraInfoPayload>();
|
private _requestIdToExtraInfo = new Map<string, Protocol.Network.requestWillBeSentExtraInfoPayload>();
|
||||||
|
private _responseExtraInfoTracker = new ResponseExtraInfoTracker();
|
||||||
|
|
||||||
constructor(client: CRSession, page: Page, parentManager: CRNetworkManager | null) {
|
constructor(client: CRSession, page: Page, parentManager: CRNetworkManager | null) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
@ -54,6 +55,7 @@ export class CRNetworkManager {
|
|||||||
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, workerFrame)),
|
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, workerFrame)),
|
||||||
eventsHelper.addEventListener(session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)),
|
eventsHelper.addEventListener(session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)),
|
||||||
eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)),
|
eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)),
|
||||||
|
eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
|
||||||
eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
|
eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
|
||||||
eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this)),
|
eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this)),
|
||||||
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page._frameManager.onWebSocketCreated(e.requestId, e.url)),
|
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page._frameManager.onWebSocketCreated(e.requestId, e.url)),
|
||||||
@ -116,6 +118,8 @@ export class CRNetworkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onRequestWillBeSent(workerFrame: frames.Frame | undefined, event: Protocol.Network.requestWillBeSentPayload) {
|
_onRequestWillBeSent(workerFrame: frames.Frame | undefined, event: Protocol.Network.requestWillBeSentPayload) {
|
||||||
|
this._responseExtraInfoTracker.requestWillBeSent(event);
|
||||||
|
|
||||||
// Request interception doesn't happen for data URLs with Network Service.
|
// Request interception doesn't happen for data URLs with Network Service.
|
||||||
if (this._protocolRequestInterceptionEnabled && !event.request.url.startsWith('data:')) {
|
if (this._protocolRequestInterceptionEnabled && !event.request.url.startsWith('data:')) {
|
||||||
const requestId = event.requestId;
|
const requestId = event.requestId;
|
||||||
@ -327,6 +331,7 @@ export class CRNetworkManager {
|
|||||||
validFrom: responsePayload?.securityDetails?.validFrom,
|
validFrom: responsePayload?.securityDetails?.validFrom,
|
||||||
validTo: responsePayload?.securityDetails?.validTo,
|
validTo: responsePayload?.securityDetails?.validTo,
|
||||||
});
|
});
|
||||||
|
this._responseExtraInfoTracker.processResponse(request._requestId, response, request.wasFulfilled());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,10 +342,16 @@ export class CRNetworkManager {
|
|||||||
if (request._interceptionId)
|
if (request._interceptionId)
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this._page._frameManager.requestReceivedResponse(response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onResponseReceivedExtraInfo(event: Protocol.Network.responseReceivedExtraInfoPayload) {
|
||||||
|
this._responseExtraInfoTracker.responseReceivedExtraInfo(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
||||||
|
this._responseExtraInfoTracker.responseReceived(event);
|
||||||
|
|
||||||
const request = this._requestIdToRequest.get(event.requestId);
|
const request = this._requestIdToRequest.get(event.requestId);
|
||||||
// FileUpload sends a response without a matching request.
|
// FileUpload sends a response without a matching request.
|
||||||
if (!request)
|
if (!request)
|
||||||
@ -350,6 +361,8 @@ export class CRNetworkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
|
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
|
||||||
|
this._responseExtraInfoTracker.loadingFinished(event);
|
||||||
|
|
||||||
let request = this._requestIdToRequest.get(event.requestId);
|
let request = this._requestIdToRequest.get(event.requestId);
|
||||||
if (!request)
|
if (!request)
|
||||||
request = this._maybeAdoptMainRequest(event.requestId);
|
request = this._maybeAdoptMainRequest(event.requestId);
|
||||||
@ -369,10 +382,12 @@ export class CRNetworkManager {
|
|||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
if (request._interceptionId)
|
if (request._interceptionId)
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||||
|
this._responseExtraInfoTracker.loadingFailed(event);
|
||||||
|
|
||||||
let request = this._requestIdToRequest.get(event.requestId);
|
let request = this._requestIdToRequest.get(event.requestId);
|
||||||
if (!request)
|
if (!request)
|
||||||
request = this._maybeAdoptMainRequest(event.requestId);
|
request = this._maybeAdoptMainRequest(event.requestId);
|
||||||
@ -410,11 +425,11 @@ export class CRNetworkManager {
|
|||||||
|
|
||||||
class InterceptableRequest {
|
class InterceptableRequest {
|
||||||
readonly request: network.Request;
|
readonly request: network.Request;
|
||||||
_requestId: string;
|
readonly _requestId: string;
|
||||||
_interceptionId: string | null;
|
readonly _interceptionId: string | null;
|
||||||
_documentId: string | undefined;
|
readonly _documentId: string | undefined;
|
||||||
_timestamp: number;
|
readonly _timestamp: number;
|
||||||
_wallTime: number;
|
readonly _wallTime: number;
|
||||||
private _route: RouteImpl | null;
|
private _route: RouteImpl | null;
|
||||||
private _redirectedFrom: InterceptableRequest | null;
|
private _redirectedFrom: InterceptableRequest | null;
|
||||||
|
|
||||||
@ -455,6 +470,10 @@ class InterceptableRequest {
|
|||||||
request = request._redirectedFrom;
|
request = request._redirectedFrom;
|
||||||
return request._route;
|
return request._route;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wasFulfilled() {
|
||||||
|
return this._routeForRedirectChain()?._wasFulfilled || false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RouteImpl implements network.RouteDelegate {
|
class RouteImpl implements network.RouteDelegate {
|
||||||
@ -463,6 +482,7 @@ class RouteImpl implements network.RouteDelegate {
|
|||||||
private _responseInterceptedPromise: Promise<Protocol.Fetch.requestPausedPayload>;
|
private _responseInterceptedPromise: Promise<Protocol.Fetch.requestPausedPayload>;
|
||||||
_responseInterceptedCallback: ((event: Protocol.Fetch.requestPausedPayload) => void) = () => {};
|
_responseInterceptedCallback: ((event: Protocol.Fetch.requestPausedPayload) => void) = () => {};
|
||||||
_interceptingResponse: boolean = false;
|
_interceptingResponse: boolean = false;
|
||||||
|
_wasFulfilled = false;
|
||||||
|
|
||||||
constructor(client: CRSession, interceptionId: string) {
|
constructor(client: CRSession, interceptionId: string) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
@ -501,6 +521,7 @@ class RouteImpl implements network.RouteDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(response: types.NormalizedFulfillResponse) {
|
async fulfill(response: types.NormalizedFulfillResponse) {
|
||||||
|
this._wasFulfilled = true;
|
||||||
const body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
|
const body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
|
||||||
|
|
||||||
// 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
|
||||||
@ -542,3 +563,138 @@ const errorReasons: { [reason: string]: Protocol.Network.ErrorReason } = {
|
|||||||
'timedout': 'TimedOut',
|
'timedout': 'TimedOut',
|
||||||
'failed': 'Failed',
|
'failed': 'Failed',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RequestInfo = {
|
||||||
|
requestId: string,
|
||||||
|
responseReceived: (Protocol.Network.Response | undefined)[],
|
||||||
|
responseReceivedExtraInfo: Protocol.Network.responseReceivedExtraInfoPayload[],
|
||||||
|
responses: network.Response[],
|
||||||
|
loadingFinished?: Protocol.Network.loadingFinishedPayload,
|
||||||
|
loadingFailed?: Protocol.Network.loadingFailedPayload,
|
||||||
|
sawResponseWithoutConnectionId: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
// This class aligns responses with response headers from extra info:
|
||||||
|
// - Network.requestWillBeSent, Network.responseReceived, Network.loadingFinished/loadingFailed are
|
||||||
|
// dispatched using one channel.
|
||||||
|
// - Network.requestWillBeSentExtraInfo and Network.responseReceivedExtraInfo are dispatches on
|
||||||
|
// another channel. Those channels are not associated, so events come in random order.
|
||||||
|
//
|
||||||
|
// This class will associate responses with the new headers. These extra info headers will become
|
||||||
|
// available to client reliably upon requestfinished event only. It consumes CDP
|
||||||
|
// signals on one end and processResponse(network.Response) signals on the other hands. It then makes
|
||||||
|
// sure that responses have all the extra headers in place by the time request finises.
|
||||||
|
//
|
||||||
|
// The shape of the instrumentation API is deliberately following the CDP, so that it
|
||||||
|
// what clear what is called when and what this means to the tracker without extra
|
||||||
|
// documentation.
|
||||||
|
class ResponseExtraInfoTracker {
|
||||||
|
private _requests = new Map<string, RequestInfo>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
requestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) {
|
||||||
|
const info = this._requests.get(event.requestId);
|
||||||
|
if (info) {
|
||||||
|
// This is redirect.
|
||||||
|
this._innerResponseReceived(info, event.redirectResponse);
|
||||||
|
} else {
|
||||||
|
this._requests.set(event.requestId, {
|
||||||
|
requestId: event.requestId,
|
||||||
|
responseReceived: [],
|
||||||
|
responseReceivedExtraInfo: [],
|
||||||
|
responses: [],
|
||||||
|
sawResponseWithoutConnectionId: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseReceived(event: Protocol.Network.responseReceivedPayload) {
|
||||||
|
const info = this._requests.get(event.requestId);
|
||||||
|
if (!info)
|
||||||
|
return;
|
||||||
|
this._innerResponseReceived(info, event.response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _innerResponseReceived(info: RequestInfo, response: Protocol.Network.Response | undefined) {
|
||||||
|
info.responseReceived.push(response);
|
||||||
|
if (!response?.connectionId) {
|
||||||
|
// Starting with this response we no longer can guarantee that response and extra info correspond to the same index.
|
||||||
|
info.sawResponseWithoutConnectionId = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseReceivedExtraInfo(event: Protocol.Network.responseReceivedExtraInfoPayload) {
|
||||||
|
const info = this._requests.get(event.requestId);
|
||||||
|
if (!info)
|
||||||
|
return;
|
||||||
|
info.responseReceivedExtraInfo.push(event);
|
||||||
|
this._patchResponseHeaders(info, info.responseReceivedExtraInfo.length - 1);
|
||||||
|
this._checkFinished(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
processResponse(requestId: string, response: network.Response, wasFulfilled: boolean) {
|
||||||
|
// We are not interested in ExtraInfo tracking for fulfilled requests, our Blink
|
||||||
|
// headers are the ones that contain fulfilled headers.
|
||||||
|
if (wasFulfilled) {
|
||||||
|
this._stopTracking(requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = this._requests.get(requestId);
|
||||||
|
if (!info || info.sawResponseWithoutConnectionId)
|
||||||
|
return;
|
||||||
|
response.setWillReceiveExtraHeaders();
|
||||||
|
info.responses.push(response);
|
||||||
|
this._patchResponseHeaders(info, info.responses.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingFinished(event: Protocol.Network.loadingFinishedPayload) {
|
||||||
|
const info = this._requests.get(event.requestId);
|
||||||
|
if (!info)
|
||||||
|
return;
|
||||||
|
info.loadingFinished = event;
|
||||||
|
this._checkFinished(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||||
|
const info = this._requests.get(event.requestId);
|
||||||
|
if (!info)
|
||||||
|
return;
|
||||||
|
info.loadingFailed = event;
|
||||||
|
this._checkFinished(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _patchResponseHeaders(info: RequestInfo, index: number) {
|
||||||
|
const response = info.responses[index];
|
||||||
|
const extraInfo = info.responseReceivedExtraInfo[index];
|
||||||
|
if (response && extraInfo)
|
||||||
|
response.extraHeadersReceived(headersObjectToArray(extraInfo.headers));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _checkFinished(info: RequestInfo) {
|
||||||
|
if (!info.loadingFinished && !info.loadingFailed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Loading finished, check that we have all ExtraInfo events in place.
|
||||||
|
if (!info.responseReceived.length) {
|
||||||
|
// loading finished without responses, finish it immediately.
|
||||||
|
this._stopTracking(info.requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could have more extra infos because we stopped collecting responses at some point.
|
||||||
|
if (info.responses.length <= info.responseReceivedExtraInfo.length) {
|
||||||
|
// We have extra info for each response.
|
||||||
|
this._stopTracking(info.requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are not done yet.
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopTracking(requestId: string) {
|
||||||
|
this._requests.delete(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -128,7 +128,7 @@ export class FFNetworkManager {
|
|||||||
this._requests.delete(request._id);
|
this._requests.delete(request._id);
|
||||||
response._requestFinished(this._relativeTiming(event.responseEndTime), undefined);
|
response._requestFinished(this._relativeTiming(event.responseEndTime), undefined);
|
||||||
}
|
}
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestFailed(event: Protocol.Network.requestFailedPayload) {
|
_onRequestFailed(event: Protocol.Network.requestFailedPayload) {
|
||||||
|
@ -74,7 +74,6 @@ export class FrameManager {
|
|||||||
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
|
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
|
||||||
readonly _signalBarriers = new Set<SignalBarrier>();
|
readonly _signalBarriers = new Set<SignalBarrier>();
|
||||||
private _webSockets = new Map<string, network.WebSocket>();
|
private _webSockets = new Map<string, network.WebSocket>();
|
||||||
readonly _responses: network.Response[] = [];
|
|
||||||
_dialogCounter = 0;
|
_dialogCounter = 0;
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
@ -204,7 +203,6 @@ export class FrameManager {
|
|||||||
frame._onClearLifecycle();
|
frame._onClearLifecycle();
|
||||||
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument };
|
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument };
|
||||||
frame.emit(Frame.Events.Navigation, navigationEvent);
|
frame.emit(Frame.Events.Navigation, navigationEvent);
|
||||||
this._responses.length = 0;
|
|
||||||
if (!initial) {
|
if (!initial) {
|
||||||
debugLogger.log('api', ` navigated to "${url}"`);
|
debugLogger.log('api', ` navigated to "${url}"`);
|
||||||
this._page.frameNavigatedToNewDocument(frame);
|
this._page.frameNavigatedToNewDocument(frame);
|
||||||
@ -274,15 +272,21 @@ export class FrameManager {
|
|||||||
requestReceivedResponse(response: network.Response) {
|
requestReceivedResponse(response: network.Response) {
|
||||||
if (response.request()._isFavicon)
|
if (response.request()._isFavicon)
|
||||||
return;
|
return;
|
||||||
this._responses.push(response);
|
|
||||||
this._page._browserContext.emit(BrowserContext.Events.Response, response);
|
this._page._browserContext.emit(BrowserContext.Events.Response, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestFinished(request: network.Request) {
|
requestFinished(request: network.Request, response: network.Response | null) {
|
||||||
this._inflightRequestFinished(request);
|
this._inflightRequestFinished(request);
|
||||||
if (request._isFavicon)
|
if (request._isFavicon)
|
||||||
return;
|
return;
|
||||||
this._page._browserContext.emit(BrowserContext.Events.RequestFinished, request);
|
this._dispatchRequestFinished(request, response).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _dispatchRequestFinished(request: network.Request, response: network.Response | null) {
|
||||||
|
// Avoid unnecessary microtask, we want to report finished early for regular redirects.
|
||||||
|
if (response?.willWaitForExtraHeaders())
|
||||||
|
await response?.waitForExtraHeadersIfNeeded();
|
||||||
|
this._page._browserContext.emit(BrowserContext.Events.RequestFinished,{ request, response });
|
||||||
}
|
}
|
||||||
|
|
||||||
requestFailed(request: network.Request, canceled: boolean) {
|
requestFailed(request: network.Request, canceled: boolean) {
|
||||||
|
@ -329,6 +329,7 @@ export class Response extends SdkObject {
|
|||||||
private _timing: ResourceTiming;
|
private _timing: ResourceTiming;
|
||||||
private _serverAddrPromise = new ManualPromise<RemoteAddr | undefined>();
|
private _serverAddrPromise = new ManualPromise<RemoteAddr | undefined>();
|
||||||
private _securityDetailsPromise = new ManualPromise<SecurityDetails | undefined>();
|
private _securityDetailsPromise = new ManualPromise<SecurityDetails | undefined>();
|
||||||
|
private _extraHeadersPromise: ManualPromise<void> | undefined;
|
||||||
private _httpVersion: string | undefined;
|
private _httpVersion: string | undefined;
|
||||||
|
|
||||||
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback, httpVersion?: string) {
|
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback, httpVersion?: string) {
|
||||||
@ -363,6 +364,25 @@ export class Response extends SdkObject {
|
|||||||
this._httpVersion = httpVersion;
|
this._httpVersion = httpVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setWillReceiveExtraHeaders() {
|
||||||
|
this._extraHeadersPromise = new ManualPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
willWaitForExtraHeaders(): boolean {
|
||||||
|
return !!this._extraHeadersPromise && !this._extraHeadersPromise.isDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForExtraHeadersIfNeeded(): Promise<void> {
|
||||||
|
await this._extraHeadersPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
extraHeadersReceived(headers: types.HeadersArray) {
|
||||||
|
this._headers = headers;
|
||||||
|
for (const { name, value } of this._headers)
|
||||||
|
this._headersMap.set(name.toLowerCase(), value);
|
||||||
|
this._extraHeadersPromise?.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
url(): string {
|
url(): string {
|
||||||
return this._url;
|
return this._url;
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ export class HarTracer {
|
|||||||
this._eventListeners = [
|
this._eventListeners = [
|
||||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._ensurePageEntry(page)),
|
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._ensurePageEntry(page)),
|
||||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)),
|
eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)),
|
||||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, (request: network.Request) => this._onRequestFinished(request).catch(() => {})),
|
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})),
|
||||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response)),
|
eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -193,14 +193,13 @@ export class HarTracer {
|
|||||||
this._delegate.onEntryStarted(harEntry);
|
this._delegate.onEntryStarted(harEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _onRequestFinished(request: network.Request) {
|
private async _onRequestFinished(request: network.Request, response: network.Response | null) {
|
||||||
|
if (!response)
|
||||||
|
return;
|
||||||
const page = request.frame()._page;
|
const page = request.frame()._page;
|
||||||
const harEntry = this._entryForRequest(request);
|
const harEntry = this._entryForRequest(request);
|
||||||
if (!harEntry)
|
if (!harEntry)
|
||||||
return;
|
return;
|
||||||
const response = await request.response();
|
|
||||||
if (!response)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const httpVersion = response.httpVersion();
|
const httpVersion = response.httpVersion();
|
||||||
const transferSize = response.transferSize() || -1;
|
const transferSize = response.transferSize() || -1;
|
||||||
|
@ -981,7 +981,7 @@ export class WKPage implements PageDelegate {
|
|||||||
response._requestFinished(responsePayload.timing ? helper.secondsToRoundishMillis(timestamp - request._timestamp) : -1, 'Response body is unavailable for redirect responses');
|
response._requestFinished(responsePayload.timing ? helper.secondsToRoundishMillis(timestamp - request._timestamp) : -1, 'Response body is unavailable for redirect responses');
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._page._frameManager.requestReceivedResponse(response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestIntercepted(session: WKSession, event: Protocol.Network.requestInterceptedPayload) {
|
_onRequestIntercepted(session: WKSession, event: Protocol.Network.requestInterceptedPayload) {
|
||||||
@ -1056,7 +1056,7 @@ export class WKPage implements PageDelegate {
|
|||||||
|
|
||||||
this._requestIdToResponseReceivedPayloadEvent.delete(request._requestId);
|
this._requestIdToResponseReceivedPayloadEvent.delete(request._requestId);
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||||
|
@ -396,6 +396,7 @@ export function wrapInASCIIBox(text: string, padding = 0): string {
|
|||||||
export class ManualPromise<T> extends Promise<T> {
|
export class ManualPromise<T> extends Promise<T> {
|
||||||
private _resolve!: (t: T) => void;
|
private _resolve!: (t: T) => void;
|
||||||
private _reject!: (e: Error) => void;
|
private _reject!: (e: Error) => void;
|
||||||
|
private _isDone: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let resolve: (t: T) => void;
|
let resolve: (t: T) => void;
|
||||||
@ -404,15 +405,22 @@ export class ManualPromise<T> extends Promise<T> {
|
|||||||
resolve = f;
|
resolve = f;
|
||||||
reject = r;
|
reject = r;
|
||||||
});
|
});
|
||||||
|
this._isDone = false;
|
||||||
this._resolve = resolve!;
|
this._resolve = resolve!;
|
||||||
this._reject = reject!;
|
this._reject = reject!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDone() {
|
||||||
|
return this._isDone;
|
||||||
|
}
|
||||||
|
|
||||||
resolve(t: T) {
|
resolve(t: T) {
|
||||||
|
this._isDone = true;
|
||||||
this._resolve(t);
|
this._resolve(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
reject(e: Error) {
|
reject(e: Error) {
|
||||||
|
this._isDone = true;
|
||||||
this._reject(e);
|
this._reject(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,3 +314,29 @@ it('should should set bodySize to 0 when there was no response body', async ({pa
|
|||||||
expect(response.request().sizes().responseHeadersSize).toBeGreaterThanOrEqual(150);
|
expect(response.request().sizes().responseHeadersSize).toBeGreaterThanOrEqual(150);
|
||||||
expect(response.request().sizes().responseTransferSize).toBeGreaterThanOrEqual(160);
|
expect(response.request().sizes().responseTransferSize).toBeGreaterThanOrEqual(160);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should report raw response headers in redirects', async ({ page, server, browserName }) => {
|
||||||
|
it.skip(browserName === 'webkit', `WebKit won't give us raw headers for redirects`);
|
||||||
|
server.setExtraHeaders('/redirect/1.html', { 'sec-test-header': '1.html' });
|
||||||
|
server.setExtraHeaders('/redirect/2.html', { 'sec-test-header': '2.html' });
|
||||||
|
server.setExtraHeaders('/empty.html', { 'sec-test-header': 'empty.html' });
|
||||||
|
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);
|
||||||
|
const expectedHeaders = ['1.html', '2.html', 'empty.html'];
|
||||||
|
|
||||||
|
const response = await page.goto(server.PREFIX + '/redirect/1.html');
|
||||||
|
await response.finished();
|
||||||
|
|
||||||
|
const redirectChain = [];
|
||||||
|
const headersChain = [];
|
||||||
|
for (let req = response.request(); req; req = req.redirectedFrom()) {
|
||||||
|
redirectChain.unshift(req.url());
|
||||||
|
const res = await req.response();
|
||||||
|
headersChain.unshift(res.headers()['sec-test-header']);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(redirectChain).toEqual(expectedUrls);
|
||||||
|
expect(headersChain).toEqual(expectedHeaders);
|
||||||
|
});
|
||||||
|
1
utils/testserver/index.d.ts
vendored
1
utils/testserver/index.d.ts
vendored
@ -24,6 +24,7 @@ export class TestServer {
|
|||||||
setAuth(path: string, username: string, password: string);
|
setAuth(path: string, username: string, password: string);
|
||||||
enableGzip(path: string);
|
enableGzip(path: string);
|
||||||
setCSP(path: string, csp: string);
|
setCSP(path: string, csp: string);
|
||||||
|
setExtraHeaders(path: string, headers: { [key: string]: string });
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
setRoute(path: string, handler: (message: IncomingMessage & { postBody: Promise<Buffer> }, response: ServerResponse) => void);
|
setRoute(path: string, handler: (message: IncomingMessage & { postBody: Promise<Buffer> }, response: ServerResponse) => void);
|
||||||
setRedirect(from: string, to: string);
|
setRedirect(from: string, to: string);
|
||||||
|
@ -97,6 +97,8 @@ class TestServer {
|
|||||||
this._auths = new Map();
|
this._auths = new Map();
|
||||||
/** @type {!Map<string, string>} */
|
/** @type {!Map<string, string>} */
|
||||||
this._csp = new Map();
|
this._csp = new Map();
|
||||||
|
/** @type {!Map<string, Object>} */
|
||||||
|
this._extraHeaders = new Map();
|
||||||
/** @type {!Set<string>} */
|
/** @type {!Set<string>} */
|
||||||
this._gzipRoutes = new Set();
|
this._gzipRoutes = new Set();
|
||||||
/** @type {!Map<string, !Promise>} */
|
/** @type {!Map<string, !Promise>} */
|
||||||
@ -153,6 +155,14 @@ class TestServer {
|
|||||||
this._csp.set(path, csp);
|
this._csp.set(path, csp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path
|
||||||
|
* @param {Object<string, string>} object
|
||||||
|
*/
|
||||||
|
setExtraHeaders(path, object) {
|
||||||
|
this._extraHeaders.set(path, object);
|
||||||
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
this.reset();
|
this.reset();
|
||||||
for (const socket of this._sockets)
|
for (const socket of this._sockets)
|
||||||
@ -175,7 +185,8 @@ class TestServer {
|
|||||||
*/
|
*/
|
||||||
setRedirect(from, to) {
|
setRedirect(from, to) {
|
||||||
this.setRoute(from, (req, res) => {
|
this.setRoute(from, (req, res) => {
|
||||||
res.writeHead(302, { location: to });
|
let headers = this._extraHeaders.get(req.url) || {};
|
||||||
|
res.writeHead(302, { ...headers, location: to });
|
||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -203,6 +214,7 @@ class TestServer {
|
|||||||
this._routes.clear();
|
this._routes.clear();
|
||||||
this._auths.clear();
|
this._auths.clear();
|
||||||
this._csp.clear();
|
this._csp.clear();
|
||||||
|
this._extraHeaders.clear();
|
||||||
this._gzipRoutes.clear();
|
this._gzipRoutes.clear();
|
||||||
const error = new Error('Static Server has been reset');
|
const error = new Error('Static Server has been reset');
|
||||||
for (const subscriber of this._requestSubscribers.values())
|
for (const subscriber of this._requestSubscribers.values())
|
||||||
@ -280,6 +292,12 @@ class TestServer {
|
|||||||
if (this._csp.has(pathName))
|
if (this._csp.has(pathName))
|
||||||
response.setHeader('Content-Security-Policy', this._csp.get(pathName));
|
response.setHeader('Content-Security-Policy', this._csp.get(pathName));
|
||||||
|
|
||||||
|
if (this._extraHeaders.has(pathName)) {
|
||||||
|
const object = this._extraHeaders.get(pathName);
|
||||||
|
for (const key in object)
|
||||||
|
response.setHeader(key, object[key]);
|
||||||
|
}
|
||||||
|
|
||||||
const {err, data} = await fs.promises.readFile(filePath).then(data => ({data})).catch(err => ({err}));
|
const {err, data} = await fs.promises.readFile(filePath).then(data => ({data})).catch(err => ({err}));
|
||||||
// The HTTP transaction might be already terminated after async hop here - do nothing in this case.
|
// The HTTP transaction might be already terminated after async hop here - do nothing in this case.
|
||||||
if (response.writableEnded)
|
if (response.writableEnded)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user