HTTPRequest.ts (9008B)
1 /** 2 * @license 3 * Copyright 2020 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; 7 import type {Protocol} from 'devtools-protocol'; 8 9 import type {CDPSession} from '../api/CDPSession.js'; 10 import type { 11 ContinueRequestOverrides, 12 ResponseForRequest, 13 } from '../api/HTTPRequest.js'; 14 import { 15 HTTPRequest, 16 STATUS_TEXTS, 17 type ResourceType, 18 handleError, 19 } from '../api/HTTPRequest.js'; 20 import {PageEvent} from '../api/Page.js'; 21 import {UnsupportedOperation} from '../common/Errors.js'; 22 import {stringToBase64} from '../util/encoding.js'; 23 24 import type {Request} from './core/Request.js'; 25 import type {BidiFrame} from './Frame.js'; 26 import {BidiHTTPResponse} from './HTTPResponse.js'; 27 28 export const requests = new WeakMap<Request, BidiHTTPRequest>(); 29 30 /** 31 * @internal 32 */ 33 export class BidiHTTPRequest extends HTTPRequest { 34 static from( 35 bidiRequest: Request, 36 frame: BidiFrame, 37 redirect?: BidiHTTPRequest, 38 ): BidiHTTPRequest { 39 const request = new BidiHTTPRequest(bidiRequest, frame, redirect); 40 request.#initialize(); 41 return request; 42 } 43 44 #redirectChain: BidiHTTPRequest[]; 45 #response: BidiHTTPResponse | null = null; 46 override readonly id: string; 47 readonly #frame: BidiFrame; 48 readonly #request: Request; 49 50 private constructor( 51 request: Request, 52 frame: BidiFrame, 53 redirect?: BidiHTTPRequest, 54 ) { 55 super(); 56 requests.set(request, this); 57 58 this.interception.enabled = request.isBlocked; 59 60 this.#request = request; 61 this.#frame = frame; 62 this.#redirectChain = redirect ? redirect.#redirectChain : []; 63 this.id = request.id; 64 } 65 66 override get client(): CDPSession { 67 return this.#frame.client; 68 } 69 70 #initialize() { 71 this.#request.on('redirect', request => { 72 const httpRequest = BidiHTTPRequest.from(request, this.#frame, this); 73 this.#redirectChain.push(this); 74 75 request.once('success', () => { 76 this.#frame 77 .page() 78 .trustedEmitter.emit(PageEvent.RequestFinished, httpRequest); 79 }); 80 81 request.once('error', () => { 82 this.#frame 83 .page() 84 .trustedEmitter.emit(PageEvent.RequestFailed, httpRequest); 85 }); 86 void httpRequest.finalizeInterceptions(); 87 }); 88 this.#request.once('success', data => { 89 this.#response = BidiHTTPResponse.from( 90 data, 91 this, 92 this.#frame.page().browser().cdpSupported, 93 ); 94 }); 95 this.#request.on('authenticate', this.#handleAuthentication); 96 97 this.#frame.page().trustedEmitter.emit(PageEvent.Request, this); 98 99 if (this.#hasInternalHeaderOverwrite) { 100 this.interception.handlers.push(async () => { 101 await this.continue( 102 { 103 headers: this.headers(), 104 }, 105 0, 106 ); 107 }); 108 } 109 } 110 111 override url(): string { 112 return this.#request.url; 113 } 114 115 override resourceType(): ResourceType { 116 if (!this.#frame.page().browser().cdpSupported) { 117 throw new UnsupportedOperation(); 118 } 119 return ( 120 this.#request.resourceType || 'other' 121 ).toLowerCase() as ResourceType; 122 } 123 124 override method(): string { 125 return this.#request.method; 126 } 127 128 override postData(): string | undefined { 129 if (!this.#frame.page().browser().cdpSupported) { 130 throw new UnsupportedOperation(); 131 } 132 return this.#request.postData; 133 } 134 135 override hasPostData(): boolean { 136 if (!this.#frame.page().browser().cdpSupported) { 137 throw new UnsupportedOperation(); 138 } 139 return this.#request.hasPostData; 140 } 141 142 override async fetchPostData(): Promise<string | undefined> { 143 throw new UnsupportedOperation(); 144 } 145 146 get #hasInternalHeaderOverwrite(): boolean { 147 return Boolean( 148 Object.keys(this.#extraHTTPHeaders).length || 149 Object.keys(this.#userAgentHeaders).length, 150 ); 151 } 152 153 get #extraHTTPHeaders(): Record<string, string> { 154 return this.#frame?.page()._extraHTTPHeaders ?? {}; 155 } 156 157 get #userAgentHeaders(): Record<string, string> { 158 return this.#frame?.page()._userAgentHeaders ?? {}; 159 } 160 161 override headers(): Record<string, string> { 162 const headers: Record<string, string> = {}; 163 for (const header of this.#request.headers) { 164 headers[header.name.toLowerCase()] = header.value.value; 165 } 166 return { 167 ...headers, 168 ...this.#extraHTTPHeaders, 169 ...this.#userAgentHeaders, 170 }; 171 } 172 173 override response(): BidiHTTPResponse | null { 174 return this.#response; 175 } 176 177 override failure(): {errorText: string} | null { 178 if (this.#request.error === undefined) { 179 return null; 180 } 181 return {errorText: this.#request.error}; 182 } 183 184 override isNavigationRequest(): boolean { 185 return this.#request.navigation !== undefined; 186 } 187 188 override initiator(): Protocol.Network.Initiator | undefined { 189 return { 190 ...this.#request.initiator, 191 type: this.#request.initiator?.type ?? 'other', 192 }; 193 } 194 195 override redirectChain(): BidiHTTPRequest[] { 196 return this.#redirectChain.slice(); 197 } 198 199 override frame(): BidiFrame { 200 return this.#frame; 201 } 202 203 override async continue( 204 overrides?: ContinueRequestOverrides, 205 priority?: number | undefined, 206 ): Promise<void> { 207 return await super.continue( 208 { 209 headers: this.#hasInternalHeaderOverwrite ? this.headers() : undefined, 210 ...overrides, 211 }, 212 priority, 213 ); 214 } 215 216 override async _continue( 217 overrides: ContinueRequestOverrides = {}, 218 ): Promise<void> { 219 const headers: Bidi.Network.Header[] = getBidiHeaders(overrides.headers); 220 this.interception.handled = true; 221 222 return await this.#request 223 .continueRequest({ 224 url: overrides.url, 225 method: overrides.method, 226 body: overrides.postData 227 ? { 228 type: 'base64', 229 value: stringToBase64(overrides.postData), 230 } 231 : undefined, 232 headers: headers.length > 0 ? headers : undefined, 233 }) 234 .catch(error => { 235 this.interception.handled = false; 236 return handleError(error); 237 }); 238 } 239 240 override async _abort(): Promise<void> { 241 this.interception.handled = true; 242 return await this.#request.failRequest().catch(error => { 243 this.interception.handled = false; 244 throw error; 245 }); 246 } 247 248 override async _respond( 249 response: Partial<ResponseForRequest>, 250 _priority?: number, 251 ): Promise<void> { 252 this.interception.handled = true; 253 254 let parsedBody: 255 | { 256 contentLength: number; 257 base64: string; 258 } 259 | undefined; 260 if (response.body) { 261 parsedBody = HTTPRequest.getResponse(response.body); 262 } 263 264 const headers: Bidi.Network.Header[] = getBidiHeaders(response.headers); 265 const hasContentLength = headers.some(header => { 266 return header.name === 'content-length'; 267 }); 268 269 if (response.contentType) { 270 headers.push({ 271 name: 'content-type', 272 value: { 273 type: 'string', 274 value: response.contentType, 275 }, 276 }); 277 } 278 279 if (parsedBody?.contentLength && !hasContentLength) { 280 headers.push({ 281 name: 'content-length', 282 value: { 283 type: 'string', 284 value: String(parsedBody.contentLength), 285 }, 286 }); 287 } 288 const status = response.status || 200; 289 290 return await this.#request 291 .provideResponse({ 292 statusCode: status, 293 headers: headers.length > 0 ? headers : undefined, 294 reasonPhrase: STATUS_TEXTS[status], 295 body: parsedBody?.base64 296 ? { 297 type: 'base64', 298 value: parsedBody?.base64, 299 } 300 : undefined, 301 }) 302 .catch(error => { 303 this.interception.handled = false; 304 throw error; 305 }); 306 } 307 308 #authenticationHandled = false; 309 #handleAuthentication = async () => { 310 if (!this.#frame) { 311 return; 312 } 313 const credentials = this.#frame.page()._credentials; 314 if (credentials && !this.#authenticationHandled) { 315 this.#authenticationHandled = true; 316 void this.#request.continueWithAuth({ 317 action: 'provideCredentials', 318 credentials: { 319 type: 'password', 320 username: credentials.username, 321 password: credentials.password, 322 }, 323 }); 324 } else { 325 void this.#request.continueWithAuth({ 326 action: 'cancel', 327 }); 328 } 329 }; 330 331 timing(): Bidi.Network.FetchTimingInfo { 332 return this.#request.timing(); 333 } 334 } 335 336 function getBidiHeaders(rawHeaders?: Record<string, unknown>) { 337 const headers: Bidi.Network.Header[] = []; 338 for (const [name, value] of Object.entries(rawHeaders ?? [])) { 339 if (!Object.is(value, undefined)) { 340 const values = Array.isArray(value) ? value : [value]; 341 342 for (const value of values) { 343 headers.push({ 344 name: name.toLowerCase(), 345 value: { 346 type: 'string', 347 value: String(value), 348 }, 349 }); 350 } 351 } 352 } 353 354 return headers; 355 }