HTTPResponse.ts (4711B)
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 {Frame} from '../api/Frame.js'; 9 import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js'; 10 import {ProtocolError} from '../common/Errors.js'; 11 import {SecurityDetails} from '../common/SecurityDetails.js'; 12 import {Deferred} from '../util/Deferred.js'; 13 import {stringToTypedArray} from '../util/encoding.js'; 14 15 import type {CdpHTTPRequest} from './HTTPRequest.js'; 16 17 /** 18 * @internal 19 */ 20 export class CdpHTTPResponse extends HTTPResponse { 21 #request: CdpHTTPRequest; 22 #contentPromise: Promise<Uint8Array> | null = null; 23 #bodyLoadedDeferred = Deferred.create<void, Error>(); 24 #remoteAddress: RemoteAddress; 25 #status: number; 26 #statusText: string; 27 #fromDiskCache: boolean; 28 #fromServiceWorker: boolean; 29 #headers: Record<string, string> = {}; 30 #securityDetails: SecurityDetails | null; 31 #timing: Protocol.Network.ResourceTiming | null; 32 33 constructor( 34 request: CdpHTTPRequest, 35 responsePayload: Protocol.Network.Response, 36 extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null, 37 ) { 38 super(); 39 this.#request = request; 40 41 this.#remoteAddress = { 42 ip: responsePayload.remoteIPAddress, 43 port: responsePayload.remotePort, 44 }; 45 this.#statusText = 46 this.#parseStatusTextFromExtraInfo(extraInfo) || 47 responsePayload.statusText; 48 this.#fromDiskCache = !!responsePayload.fromDiskCache; 49 this.#fromServiceWorker = !!responsePayload.fromServiceWorker; 50 51 this.#status = extraInfo ? extraInfo.statusCode : responsePayload.status; 52 const headers = extraInfo ? extraInfo.headers : responsePayload.headers; 53 for (const [key, value] of Object.entries(headers)) { 54 this.#headers[key.toLowerCase()] = value; 55 } 56 57 this.#securityDetails = responsePayload.securityDetails 58 ? new SecurityDetails(responsePayload.securityDetails) 59 : null; 60 this.#timing = responsePayload.timing || null; 61 } 62 63 #parseStatusTextFromExtraInfo( 64 extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null, 65 ): string | undefined { 66 if (!extraInfo || !extraInfo.headersText) { 67 return; 68 } 69 const firstLine = extraInfo.headersText.split('\r', 1)[0]; 70 if (!firstLine || firstLine.length > 1_000) { 71 return; 72 } 73 const match = firstLine.match(/[^ ]* [^ ]* (.*)/); 74 if (!match) { 75 return; 76 } 77 const statusText = match[1]; 78 if (!statusText) { 79 return; 80 } 81 return statusText; 82 } 83 84 _resolveBody(err?: Error): void { 85 if (err) { 86 return this.#bodyLoadedDeferred.reject(err); 87 } 88 return this.#bodyLoadedDeferred.resolve(); 89 } 90 91 override remoteAddress(): RemoteAddress { 92 return this.#remoteAddress; 93 } 94 95 override url(): string { 96 return this.#request.url(); 97 } 98 99 override status(): number { 100 return this.#status; 101 } 102 103 override statusText(): string { 104 return this.#statusText; 105 } 106 107 override headers(): Record<string, string> { 108 return this.#headers; 109 } 110 111 override securityDetails(): SecurityDetails | null { 112 return this.#securityDetails; 113 } 114 115 override timing(): Protocol.Network.ResourceTiming | null { 116 return this.#timing; 117 } 118 119 override content(): Promise<Uint8Array> { 120 if (!this.#contentPromise) { 121 this.#contentPromise = this.#bodyLoadedDeferred 122 .valueOrThrow() 123 .then(async () => { 124 try { 125 // Use CDPSession from corresponding request to retrieve body, as it's client 126 // might have been updated (e.g. for an adopted OOPIF). 127 const response = await this.#request.client.send( 128 'Network.getResponseBody', 129 { 130 requestId: this.#request.id, 131 }, 132 ); 133 134 return stringToTypedArray(response.body, response.base64Encoded); 135 } catch (error) { 136 if ( 137 error instanceof ProtocolError && 138 error.originalMessage === 139 'No resource with given identifier found' 140 ) { 141 throw new ProtocolError( 142 'Could not load body for this request. This might happen if the request is a preflight request.', 143 ); 144 } 145 146 throw error; 147 } 148 }); 149 } 150 return this.#contentPromise; 151 } 152 153 override request(): CdpHTTPRequest { 154 return this.#request; 155 } 156 157 override fromCache(): boolean { 158 return this.#fromDiskCache || this.#request._fromMemoryCache; 159 } 160 161 override fromServiceWorker(): boolean { 162 return this.#fromServiceWorker; 163 } 164 165 override frame(): Frame | null { 166 return this.#request.frame(); 167 } 168 }