HTTPRequest.ts (6970B)
1 /** 2 * @license 3 * Copyright 2020 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 import type {Protocol} from 'devtools-protocol'; 7 8 import type {CDPSession} from '../api/CDPSession.js'; 9 import type {Frame} from '../api/Frame.js'; 10 import { 11 type ContinueRequestOverrides, 12 headersArray, 13 HTTPRequest, 14 type ResourceType, 15 type ResponseForRequest, 16 STATUS_TEXTS, 17 handleError, 18 } from '../api/HTTPRequest.js'; 19 import {debugError} from '../common/util.js'; 20 import {stringToBase64} from '../util/encoding.js'; 21 22 import type {CdpHTTPResponse} from './HTTPResponse.js'; 23 24 /** 25 * @internal 26 */ 27 export class CdpHTTPRequest extends HTTPRequest { 28 override id: string; 29 declare _redirectChain: CdpHTTPRequest[]; 30 declare _response: CdpHTTPResponse | null; 31 32 #client: CDPSession; 33 #isNavigationRequest: boolean; 34 35 #url: string; 36 #resourceType: ResourceType; 37 38 #method: string; 39 #hasPostData = false; 40 #postData?: string; 41 #headers: Record<string, string> = {}; 42 #frame: Frame | null; 43 #initiator?: Protocol.Network.Initiator; 44 45 override get client(): CDPSession { 46 return this.#client; 47 } 48 49 override set client(newClient: CDPSession) { 50 this.#client = newClient; 51 } 52 53 constructor( 54 client: CDPSession, 55 frame: Frame | null, 56 interceptionId: string | undefined, 57 allowInterception: boolean, 58 data: { 59 /** 60 * Request identifier. 61 */ 62 requestId: Protocol.Network.RequestId; 63 /** 64 * Loader identifier. Empty string if the request is fetched from worker. 65 */ 66 loaderId?: Protocol.Network.LoaderId; 67 /** 68 * URL of the document this request is loaded for. 69 */ 70 documentURL?: string; 71 /** 72 * Request data. 73 */ 74 request: Protocol.Network.Request; 75 /** 76 * Request initiator. 77 */ 78 initiator?: Protocol.Network.Initiator; 79 /** 80 * Type of this resource. 81 */ 82 type?: Protocol.Network.ResourceType; 83 }, 84 redirectChain: CdpHTTPRequest[], 85 ) { 86 super(); 87 this.#client = client; 88 this.id = data.requestId; 89 this.#isNavigationRequest = 90 data.requestId === data.loaderId && data.type === 'Document'; 91 this._interceptionId = interceptionId; 92 this.#url = data.request.url + (data.request.urlFragment ?? ''); 93 this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType; 94 this.#method = data.request.method; 95 this.#postData = data.request.postData; 96 this.#hasPostData = data.request.hasPostData ?? false; 97 this.#frame = frame; 98 this._redirectChain = redirectChain; 99 this.#initiator = data.initiator; 100 101 this.interception.enabled = allowInterception; 102 103 for (const [key, value] of Object.entries(data.request.headers)) { 104 this.#headers[key.toLowerCase()] = value; 105 } 106 } 107 108 override url(): string { 109 return this.#url; 110 } 111 112 override resourceType(): ResourceType { 113 return this.#resourceType; 114 } 115 116 override method(): string { 117 return this.#method; 118 } 119 120 override postData(): string | undefined { 121 return this.#postData; 122 } 123 124 override hasPostData(): boolean { 125 return this.#hasPostData; 126 } 127 128 override async fetchPostData(): Promise<string | undefined> { 129 try { 130 const result = await this.#client.send('Network.getRequestPostData', { 131 requestId: this.id, 132 }); 133 return result.postData; 134 } catch (err) { 135 debugError(err); 136 return; 137 } 138 } 139 140 override headers(): Record<string, string> { 141 return this.#headers; 142 } 143 144 override response(): CdpHTTPResponse | null { 145 return this._response; 146 } 147 148 override frame(): Frame | null { 149 return this.#frame; 150 } 151 152 override isNavigationRequest(): boolean { 153 return this.#isNavigationRequest; 154 } 155 156 override initiator(): Protocol.Network.Initiator | undefined { 157 return this.#initiator; 158 } 159 160 override redirectChain(): CdpHTTPRequest[] { 161 return this._redirectChain.slice(); 162 } 163 164 override failure(): {errorText: string} | null { 165 if (!this._failureText) { 166 return null; 167 } 168 return { 169 errorText: this._failureText, 170 }; 171 } 172 173 /** 174 * @internal 175 */ 176 async _continue(overrides: ContinueRequestOverrides = {}): Promise<void> { 177 const {url, method, postData, headers} = overrides; 178 this.interception.handled = true; 179 180 const postDataBinaryBase64 = postData 181 ? stringToBase64(postData) 182 : undefined; 183 184 if (this._interceptionId === undefined) { 185 throw new Error( 186 'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest', 187 ); 188 } 189 await this.#client 190 .send('Fetch.continueRequest', { 191 requestId: this._interceptionId, 192 url, 193 method, 194 postData: postDataBinaryBase64, 195 headers: headers ? headersArray(headers) : undefined, 196 }) 197 .catch(error => { 198 this.interception.handled = false; 199 return handleError(error); 200 }); 201 } 202 203 async _respond(response: Partial<ResponseForRequest>): Promise<void> { 204 this.interception.handled = true; 205 206 let parsedBody: 207 | { 208 contentLength: number; 209 base64: string; 210 } 211 | undefined; 212 if (response.body) { 213 parsedBody = HTTPRequest.getResponse(response.body); 214 } 215 216 const responseHeaders: Record<string, string | string[]> = {}; 217 if (response.headers) { 218 for (const header of Object.keys(response.headers)) { 219 const value = response.headers[header]; 220 221 responseHeaders[header.toLowerCase()] = Array.isArray(value) 222 ? value.map(item => { 223 return String(item); 224 }) 225 : String(value); 226 } 227 } 228 if (response.contentType) { 229 responseHeaders['content-type'] = response.contentType; 230 } 231 if (parsedBody?.contentLength && !('content-length' in responseHeaders)) { 232 responseHeaders['content-length'] = String(parsedBody.contentLength); 233 } 234 235 const status = response.status || 200; 236 if (this._interceptionId === undefined) { 237 throw new Error( 238 'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest', 239 ); 240 } 241 await this.#client 242 .send('Fetch.fulfillRequest', { 243 requestId: this._interceptionId, 244 responseCode: status, 245 responsePhrase: STATUS_TEXTS[status], 246 responseHeaders: headersArray(responseHeaders), 247 body: parsedBody?.base64, 248 }) 249 .catch(error => { 250 this.interception.handled = false; 251 return handleError(error); 252 }); 253 } 254 255 async _abort( 256 errorReason: Protocol.Network.ErrorReason | null, 257 ): Promise<void> { 258 this.interception.handled = true; 259 if (this._interceptionId === undefined) { 260 throw new Error( 261 'HTTPRequest is missing _interceptionId needed for Fetch.failRequest', 262 ); 263 } 264 await this.#client 265 .send('Fetch.failRequest', { 266 requestId: this._interceptionId, 267 errorReason: errorReason || 'Failed', 268 }) 269 .catch(handleError); 270 } 271 }