NetworkResponse.sys.mjs (9399B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 ChromeUtils.defineESModuleGetters(lazy, { 7 NetworkUtils: 8 "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", 9 10 NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs", 11 }); 12 13 /** 14 * The NetworkResponse class is a wrapper around the internal channel which 15 * provides getters and methods closer to fetch's response concept 16 * (https://fetch.spec.whatwg.org/#concept-response). 17 */ 18 export class NetworkResponse { 19 #channel; 20 #decodedBodySize; 21 #encodedBodySize; 22 #fromCache; 23 #fromServiceWorker; 24 #isCachedResource; 25 #isDataURL; 26 #headersTransmittedSize; 27 #responseBodyReady; 28 #status; 29 #statusMessage; 30 #totalTransmittedSize; 31 #wrappedChannel; 32 33 /** 34 * 35 * @param {nsIChannel} channel 36 * The channel for the response. 37 * @param {object} params 38 * @param {boolean} params.fromCache 39 * Whether the response was read from the cache or not. 40 * @param {boolean} params.fromServiceWorker 41 * Whether the response is coming from a service worker or not. 42 * @param {boolean} params.isCachedResource 43 * Whether the response is served by the stencil (image/CSS/JS) cache. 44 * @param {string=} params.rawHeaders 45 * The response's raw (ie potentially compressed) headers 46 */ 47 constructor(channel, params) { 48 this.#channel = channel; 49 const { 50 fromCache, 51 fromServiceWorker, 52 isCachedResource, 53 rawHeaders = "", 54 } = params; 55 this.#fromCache = fromCache; 56 this.#fromServiceWorker = fromServiceWorker; 57 this.#isCachedResource = isCachedResource; 58 this.#isDataURL = this.#channel instanceof Ci.nsIDataChannel; 59 this.#responseBodyReady = Promise.withResolvers(); 60 this.#wrappedChannel = ChannelWrapper.get(channel); 61 62 this.#decodedBodySize = 0; 63 this.#encodedBodySize = 0; 64 this.#headersTransmittedSize = rawHeaders.length; 65 this.#totalTransmittedSize = rawHeaders.length; 66 67 // See https://github.com/w3c/webdriver-bidi/issues/761 68 // For 304 responses, the response will be replaced by the cached response 69 // between responseStarted and responseCompleted, which will effectively 70 // change the status and statusMessage. 71 // Until the issue linked above has been discussed and closed, we will 72 // cache the status/statusMessage in order to ensure consistent values 73 // between responseStarted and responseCompleted. 74 this.#status = this.#isDataURL ? 200 : this.#channel.responseStatus; 75 this.#statusMessage = 76 this.#isDataURL || this.#isCachedResource 77 ? "OK" 78 : this.#channel.responseStatusText; 79 } 80 81 get decodedBodySize() { 82 return this.#decodedBodySize; 83 } 84 85 get encodedBodySize() { 86 return this.#encodedBodySize; 87 } 88 89 get headers() { 90 return this.#getHeadersList(); 91 } 92 93 get headersTransmittedSize() { 94 return this.#headersTransmittedSize; 95 } 96 97 get fromCache() { 98 return this.#fromCache; 99 } 100 101 get fromServiceWorker() { 102 return this.#fromServiceWorker; 103 } 104 105 get isDataURL() { 106 return this.#isDataURL; 107 } 108 109 get mimeType() { 110 return this.#getComputedMimeType(); 111 } 112 113 get protocol() { 114 return lazy.NetworkUtils.getProtocol(this.#channel); 115 } 116 117 get serializedURL() { 118 return this.#channel.URI.spec; 119 } 120 121 get status() { 122 return this.#status; 123 } 124 125 get statusMessage() { 126 return this.#statusMessage; 127 } 128 129 get totalTransmittedSize() { 130 return this.#totalTransmittedSize; 131 } 132 133 /** 134 * Check if this response will lead to a redirect. 135 */ 136 get willRedirect() { 137 // See static helper on nsHttpChannel:WillRedirect 138 // https://searchfox.org/mozilla-central/rev/6b4cb595d05ac38e2cfc493e3b81fe4c97a71f12/netwerk/protocol/http/nsHttpChannel.cpp#283-288 139 const isRedirectStatus = 140 this.#status == 301 || 141 this.#status == 302 || 142 this.#status == 303 || 143 this.#status == 307 || 144 this.#status == 308; 145 return isRedirectStatus && this.#channel.getResponseHeader("Location"); 146 } 147 148 /** 149 * Clear a response header from the responses's headers list. 150 * 151 * @param {string} name 152 * The header's name. 153 */ 154 clearResponseHeader(name) { 155 this.#channel.setResponseHeader( 156 name, // aName 157 "", // aValue="" as an empty value 158 false // aMerge=false to force clearing the header 159 ); 160 } 161 162 /** 163 * Returns the NetworkDataBytes instance representing the response body for 164 * this response. 165 * 166 * @returns {NetworkDataBytes} 167 */ 168 readAndProcessResponseBody = async () => { 169 const responseContent = await this.#responseBodyReady.promise; 170 171 return new lazy.NetworkDataBytes({ 172 getBytesValue: async () => { 173 if (responseContent.isContentEncoded) { 174 return lazy.NetworkUtils.decodeResponseChunks( 175 responseContent.encodedData, 176 { 177 // Should always attempt to decode as UTF-8. 178 charset: "UTF-8", 179 compressionEncodings: responseContent.compressionEncodings, 180 encodedBodySize: responseContent.encodedBodySize, 181 encoding: responseContent.encoding, 182 } 183 ); 184 } 185 return responseContent.text; 186 }, 187 isBase64: responseContent.encoding === "base64", 188 }); 189 }; 190 191 setResponseContent(responseContent) { 192 this.#responseBodyReady.resolve(responseContent); 193 } 194 195 /** 196 * Set a response header 197 * 198 * @param {string} name 199 * The header's name. 200 * @param {string} value 201 * The header's value. 202 * @param {object} options 203 * @param {boolean} options.merge 204 * True if the value should be merged with the existing value, false if it 205 * should override it. Defaults to false. 206 */ 207 setResponseHeader(name, value, options) { 208 const { merge = false } = options; 209 this.#channel.setResponseHeader(name, value, merge); 210 } 211 212 setResponseStatus(options) { 213 let { status, statusText } = options; 214 if (status === null) { 215 status = this.#channel.responseStatus; 216 } 217 218 if (statusText === null) { 219 statusText = this.#channel.responseStatusText; 220 } 221 222 this.#channel.setResponseStatus(status, statusText); 223 224 // Update the cached status and statusMessage. 225 this.#status = this.#channel.responseStatus; 226 this.#statusMessage = this.#channel.responseStatusText; 227 } 228 229 /** 230 * Set the various response sizes for this response. Depending on how the 231 * completion was monitored (DevTools NetworkResponseListener or ChannelWrapper 232 * event), sizes need to be retrieved differently. 233 * There this is a simple setter and the actual logic to retrieve sizes is in 234 * NetworkEventRecord. 235 * 236 * @param {object} sizes 237 * @param {number} sizes.decodedBodySize 238 * The decoded body size. 239 * @param {number} sizes.encodedBodySize 240 * The encoded body size. 241 * @param {number} sizes.totalTransmittedSize 242 * The total transmitted size. 243 */ 244 setResponseSizes(sizes) { 245 const { decodedBodySize, encodedBodySize, totalTransmittedSize } = sizes; 246 this.#decodedBodySize = decodedBodySize; 247 this.#encodedBodySize = encodedBodySize; 248 this.#totalTransmittedSize = totalTransmittedSize; 249 } 250 251 /** 252 * Return a static version of the class instance. 253 * This method is used to prepare the data to be sent with the events for cached resources 254 * generated from the content process but need to be sent to the parent. 255 */ 256 toJSON() { 257 return { 258 decodedBodySize: this.decodedBodySize, 259 encodedBodySize: this.encodedBodySize, 260 fromCache: this.fromCache, 261 headers: this.headers, 262 headersTransmittedSize: this.headersTransmittedSize, 263 isDataURL: this.isDataURL, 264 mimeType: this.mimeType, 265 protocol: this.protocol, 266 serializedURL: this.serializedURL, 267 status: this.status, 268 statusMessage: this.statusMessage, 269 totalTransmittedSize: this.totalTransmittedSize, 270 willRedirect: this.willRedirect, 271 }; 272 } 273 274 #getComputedMimeType() { 275 // TODO: DevTools NetworkObserver is computing a similar value in 276 // addResponseContent, but uses an inconsistent implementation in 277 // addResponseStart. This approach can only be used as early as in 278 // addResponseHeaders. We should move this logic to the NetworkObserver and 279 // expose mimeType in addResponseStart. Bug 1809670. 280 let mimeType = ""; 281 282 try { 283 if (this.#isDataURL || this.#isCachedResource) { 284 mimeType = this.#channel.contentType; 285 } else { 286 mimeType = this.#wrappedChannel.contentType; 287 } 288 const contentCharset = this.#channel.contentCharset; 289 if (contentCharset) { 290 mimeType += `;charset=${contentCharset}`; 291 } 292 } catch (e) { 293 // Ignore exceptions when reading contentType/contentCharset 294 } 295 296 return mimeType; 297 } 298 299 #getHeadersList() { 300 const headers = []; 301 302 // According to the fetch spec for data URLs we can just hardcode 303 // "Content-Type" header. 304 if (this.#isDataURL) { 305 headers.push(["Content-Type", this.#channel.contentType]); 306 } else { 307 this.#channel.visitResponseHeaders({ 308 visitHeader(name, value) { 309 headers.push([name, value]); 310 }, 311 }); 312 } 313 314 return headers; 315 } 316 }