| /** |
| * @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; |
| } |
| |
| 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; |
| 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); |
| } |
| } |