302 lines
7.6 KiB
TypeScript
302 lines
7.6 KiB
TypeScript
/**
|
|
* @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 {
|
|
mergeUint8Arrays,
|
|
stringToBase64,
|
|
stringToTypedArray,
|
|
} 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;
|
|
}
|
|
|
|
override set client(newClient: CDPSession) {
|
|
this.#client = newClient;
|
|
}
|
|
|
|
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 + (data.request.urlFragment ?? '');
|
|
this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType;
|
|
this.#method = data.request.method;
|
|
if (
|
|
data.request.postDataEntries &&
|
|
data.request.postDataEntries.length > 0
|
|
) {
|
|
this.#postData = new TextDecoder().decode(
|
|
mergeUint8Arrays(
|
|
data.request.postDataEntries
|
|
.map(entry => {
|
|
return entry.bytes ? stringToTypedArray(entry.bytes, true) : null;
|
|
})
|
|
.filter((entry): entry is Uint8Array => {
|
|
return entry !== null;
|
|
}),
|
|
),
|
|
);
|
|
} else {
|
|
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;
|
|
|
|
this.updateHeaders(data.request.headers);
|
|
}
|
|
|
|
updateHeaders(headers: Protocol.Network.Headers): void {
|
|
for (const [key, value] of Object.entries(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> {
|
|
// Callers should not be allowed to mutate internal structure.
|
|
return structuredClone(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,
|
|
};
|
|
}
|
|
|
|
protected canBeIntercepted(): boolean {
|
|
return !this.url().startsWith('data:') && !this._fromMemoryCache;
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
}
|