Source code

Revision control

Copy as Markdown

Other Tools

/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import type {Frame} from '../api/Frame.js';
import {
type ContinueRequestOverrides,
headersArray,
HTTPRequest,
type ResourceType,
type ResponseForRequest,
STATUS_TEXTS,
handleError,
} from '../api/HTTPRequest.js';
import {debugError} from '../common/util.js';
import {stringToBase64} from '../util/encoding.js';
import type {CdpHTTPResponse} from './HTTPResponse.js';
/**
* @internal
*/
export class CdpHTTPRequest extends HTTPRequest {
override id: string;
declare _redirectChain: CdpHTTPRequest[];
declare _response: CdpHTTPResponse | null;
#client: CDPSession;
#isNavigationRequest: boolean;
#url: string;
#resourceType: ResourceType;
#method: string;
#hasPostData = false;
#postData?: string;
#headers: Record<string, string> = {};
#frame: Frame | null;
#initiator?: Protocol.Network.Initiator;
override get client(): CDPSession {
return this.#client;
}
constructor(
client: CDPSession,
frame: Frame | null,
interceptionId: string | undefined,
allowInterception: boolean,
data: {
/**
* Request identifier.
*/
requestId: Protocol.Network.RequestId;
/**
* Loader identifier. Empty string if the request is fetched from worker.
*/
loaderId?: Protocol.Network.LoaderId;
/**
* URL of the document this request is loaded for.
*/
documentURL?: string;
/**
* Request data.
*/
request: Protocol.Network.Request;
/**
* Request initiator.
*/
initiator?: Protocol.Network.Initiator;
/**
* Type of this resource.
*/
type?: Protocol.Network.ResourceType;
},
redirectChain: CdpHTTPRequest[]
) {
super();
this.#client = client;
this.id = data.requestId;
this.#isNavigationRequest =
data.requestId === data.loaderId && data.type === 'Document';
this._interceptionId = interceptionId;
this.#url = data.request.url;
this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType;
this.#method = data.request.method;
this.#postData = data.request.postData;
this.#hasPostData = data.request.hasPostData ?? false;
this.#frame = frame;
this._redirectChain = redirectChain;
this.#initiator = data.initiator;
this.interception.enabled = allowInterception;
for (const [key, value] of Object.entries(data.request.headers)) {
this.#headers[key.toLowerCase()] = value;
}
}
override url(): string {
return this.#url;
}
override resourceType(): ResourceType {
return this.#resourceType;
}
override method(): string {
return this.#method;
}
override postData(): string | undefined {
return this.#postData;
}
override hasPostData(): boolean {
return this.#hasPostData;
}
override async fetchPostData(): Promise<string | undefined> {
try {
const result = await this.#client.send('Network.getRequestPostData', {
requestId: this.id,
});
return result.postData;
} catch (err) {
debugError(err);
return;
}
}
override headers(): Record<string, string> {
return this.#headers;
}
override response(): CdpHTTPResponse | null {
return this._response;
}
override frame(): Frame | null {
return this.#frame;
}
override isNavigationRequest(): boolean {
return this.#isNavigationRequest;
}
override initiator(): Protocol.Network.Initiator | undefined {
return this.#initiator;
}
override redirectChain(): CdpHTTPRequest[] {
return this._redirectChain.slice();
}
override failure(): {errorText: string} | null {
if (!this._failureText) {
return null;
}
return {
errorText: this._failureText,
};
}
/**
* @internal
*/
async _continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
const {url, method, postData, headers} = overrides;
this.interception.handled = true;
const postDataBinaryBase64 = postData
? stringToBase64(postData)
: undefined;
if (this._interceptionId === undefined) {
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest'
);
}
await this.#client
.send('Fetch.continueRequest', {
requestId: this._interceptionId,
url,
method,
postData: postDataBinaryBase64,
headers: headers ? headersArray(headers) : undefined,
})
.catch(error => {
this.interception.handled = false;
return handleError(error);
});
}
async _respond(response: Partial<ResponseForRequest>): Promise<void> {
this.interception.handled = true;
let parsedBody:
| {
contentLength: number;
base64: string;
}
| undefined;
if (response.body) {
parsedBody = HTTPRequest.getResponse(response.body);
}
const responseHeaders: Record<string, string | string[]> = {};
if (response.headers) {
for (const header of Object.keys(response.headers)) {
const value = response.headers[header];
responseHeaders[header.toLowerCase()] = Array.isArray(value)
? value.map(item => {
return String(item);
})
: String(value);
}
}
if (response.contentType) {
responseHeaders['content-type'] = response.contentType;
}
if (parsedBody?.contentLength && !('content-length' in responseHeaders)) {
responseHeaders['content-length'] = String(parsedBody.contentLength);
}
const status = response.status || 200;
if (this._interceptionId === undefined) {
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest'
);
}
await this.#client
.send('Fetch.fulfillRequest', {
requestId: this._interceptionId,
responseCode: status,
responsePhrase: STATUS_TEXTS[status],
responseHeaders: headersArray(responseHeaders),
body: parsedBody?.base64,
})
.catch(error => {
this.interception.handled = false;
return handleError(error);
});
}
async _abort(
errorReason: Protocol.Network.ErrorReason | null
): Promise<void> {
this.interception.handled = true;
if (this._interceptionId === undefined) {
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest'
);
}
await this.#client
.send('Fetch.failRequest', {
requestId: this._interceptionId,
errorReason: errorReason || 'Failed',
})
.catch(handleError);
}
}