UNPKG

chrome-devtools-frontend

Version:
1,406 lines (1,227 loc) • 79.7 kB
// Copyright 2012 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Protocol from '../../generated/protocol.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import {Attribute, type Cookie} from './Cookie.js'; import {CookieModel} from './CookieModel.js'; import {CookieParser} from './CookieParser.js'; import * as HttpReasonPhraseStrings from './HttpReasonPhraseStrings.js'; import { Events as NetworkManagerEvents, NetworkManager, } from './NetworkManager.js'; import {ServerSentEvents} from './ServerSentEvents.js'; import {ServerTiming} from './ServerTiming.js'; import {Type} from './Target.js'; // clang-format off const UIStrings = { /** * @description Text in Network Request */ binary: '(binary)', /** * @description Tooltip to explain why a cookie was blocked */ secureOnly: 'This cookie was blocked because it had the "`Secure`" attribute and the connection was not secure.', /** * @description Tooltip to explain why a cookie was blocked */ notOnPath: 'This cookie was blocked because its path was not an exact match for or a superdirectory of the request url\'s path.', /** * @description Tooltip to explain why a cookie was blocked */ domainMismatch: 'This cookie was blocked because neither did the request URL\'s domain exactly match the cookie\'s domain, nor was the request URL\'s domain a subdomain of the cookie\'s Domain attribute value.', /** * @description Tooltip to explain why a cookie was blocked */ sameSiteStrict: 'This cookie was blocked because it had the "`SameSite=Strict`" attribute and the request was made from a different site. This includes top-level navigation requests initiated by other sites.', /** * @description Tooltip to explain why a cookie was blocked */ sameSiteLax: 'This cookie was blocked because it had the "`SameSite=Lax`" attribute and the request was made from a different site and was not initiated by a top-level navigation.', /** * @description Tooltip to explain why a cookie was blocked */ sameSiteUnspecifiedTreatedAsLax: 'This cookie didn\'t specify a "`SameSite`" attribute when it was stored and was defaulted to "SameSite=Lax," and was blocked because the request was made from a different site and was not initiated by a top-level navigation. The cookie had to have been set with "`SameSite=None`" to enable cross-site usage.', /** * @description Tooltip to explain why a cookie was blocked */ sameSiteNoneInsecure: 'This cookie was blocked because it had the "`SameSite=None`" attribute but was not marked "Secure". Cookies without SameSite restrictions must be marked "Secure" and sent over a secure connection.', /** * @description Tooltip to explain why a cookie was blocked */ userPreferences: 'This cookie was blocked due to user preferences.', /** * @description Tooltip to explain why a cookie was blocked */ thirdPartyPhaseout: 'This cookie was blocked either because of Chrome flags or browser configuration. Learn more in the Issues panel.', /** * @description Tooltip to explain why a cookie was blocked */ unknownError: 'An unknown error was encountered when trying to send this cookie.', /** * @description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site */ schemefulSameSiteStrict: 'This cookie was blocked because it had the "`SameSite=Strict`" attribute but the request was cross-site. This includes top-level navigation requests initiated by other sites. This request is considered cross-site because the URL has a different scheme than the current site.', /** * @description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site */ schemefulSameSiteLax: 'This cookie was blocked because it had the "`SameSite=Lax`" attribute but the request was cross-site and was not initiated by a top-level navigation. This request is considered cross-site because the URL has a different scheme than the current site.', /** * @description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site */ schemefulSameSiteUnspecifiedTreatedAsLax: 'This cookie didn\'t specify a "`SameSite`" attribute when it was stored, was defaulted to "`SameSite=Lax"`, and was blocked because the request was cross-site and was not initiated by a top-level navigation. This request is considered cross-site because the URL has a different scheme than the current site.', /** * @description Tooltip to explain why a cookie was blocked due to SameParty */ samePartyFromCrossPartyContext: 'This cookie was blocked because it had the "`SameParty`" attribute but the request was cross-party. The request was considered cross-party because the domain of the resource\'s URL and the domains of the resource\'s enclosing frames/documents are neither owners nor members in the same First-Party Set.', /** * @description Tooltip to explain why a cookie was blocked due to exceeding the maximum size */ nameValuePairExceedsMaxSize: 'This cookie was blocked because it was too large. The combined size of the name and value must be less than or equal to 4096 characters.', /** * @description Tooltip to explain why an attempt to set a cookie via `Set-Cookie` HTTP header on a request's response was blocked. */ thisSetcookieWasBlockedDueToUser: 'This attempt to set a cookie via a `Set-Cookie` header was blocked due to user preferences.', /** * @description Tooltip to explain why an attempt to set a cookie via `Set-Cookie` HTTP header on a request's response was blocked. */ thisSetcookieWasBlockedDueThirdPartyPhaseout: 'Setting this cookie was blocked either because of Chrome flags or browser configuration. Learn more in the Issues panel.', /** * @description Tooltip to explain why an attempt to set a cookie via `Set-Cookie` HTTP header on a request's response was blocked. */ thisSetcookieHadInvalidSyntax: 'This `Set-Cookie` header had invalid syntax.', /** * @description Tooltip to explain why a cookie was blocked */ thisSetcookieHadADisallowedCharacter: 'This `Set-Cookie` header contained a disallowed character (a forbidden ASCII control character, or the tab character if it appears in the middle of the cookie name, value, an attribute name, or an attribute value).', /** * @description Tooltip to explain why a cookie was blocked */ theSchemeOfThisConnectionIsNot: 'The scheme of this connection is not allowed to store cookies.', /** * @description Tooltip to explain why a cookie was blocked */ anUnknownErrorWasEncounteredWhenTrying: 'An unknown error was encountered when trying to store this cookie.', /** * @description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site * @example {SameSite=Strict} PH1 */ thisSetcookieWasBlockedBecauseItHadTheSamesiteStrictLax: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "{PH1}" attribute but came from a cross-site response which was not the response to a top-level navigation. This response is considered cross-site because the URL has a different scheme than the current site.', /** * @description Tooltip to explain why a cookie was blocked due to Schemeful Same-Site */ thisSetcookieDidntSpecifyASamesite: 'This `Set-Cookie` header didn\'t specify a "`SameSite`" attribute, was defaulted to "`SameSite=Lax"`, and was blocked because it came from a cross-site response which was not the response to a top-level navigation. This response is considered cross-site because the URL has a different scheme than the current site.', /** * @description Tooltip to explain why a cookie was blocked due to SameParty */ thisSetcookieWasBlockedBecauseItHadTheSameparty: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "`SameParty`" attribute but the request was cross-party. The request was considered cross-party because the domain of the resource\'s URL and the domains of the resource\'s enclosing frames/documents are neither owners nor members in the same First-Party Set.', /** * @description Tooltip to explain why a cookie was blocked due to SameParty */ thisSetcookieWasBlockedBecauseItHadTheSamepartyAttribute: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "`SameParty`" attribute but also had other conflicting attributes. Chrome requires cookies that use the "`SameParty`" attribute to also have the "Secure" attribute, and to not be restricted to "`SameSite=Strict`".', /** * @description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonSecureOnly: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "Secure" attribute but was not received over a secure connection.', /** * @description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. * @example {SameSite=Strict} PH1 */ blockedReasonSameSiteStrictLax: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "{PH1}" attribute but came from a cross-site response which was not the response to a top-level navigation.', /** * @description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonSameSiteUnspecifiedTreatedAsLax: 'This `Set-Cookie` header didn\'t specify a "`SameSite`" attribute and was defaulted to "`SameSite=Lax,`" and was blocked because it came from a cross-site response which was not the response to a top-level navigation. The `Set-Cookie` had to have been set with "`SameSite=None`" to enable cross-site usage.', /** * @description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonSameSiteNoneInsecure: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it had the "`SameSite=None`" attribute but did not have the "Secure" attribute, which is required in order to use "`SameSite=None`".', /** * @description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonOverwriteSecure: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it was not sent over a secure connection and would have overwritten a cookie with the Secure attribute.', /** * @description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonInvalidDomain: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because its Domain attribute was invalid with regards to the current host url.', /** * @description Tooltip to explain why an attempt to set a cookie via a `Set-Cookie` HTTP header on a request's response was blocked. */ blockedReasonInvalidPrefix: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because it used the "`__Secure-`" or "`__Host-`" prefix in its name and broke the additional rules applied to cookies with these prefixes as defined in `https://tools.ietf.org/html/draft-west-cookie-prefixes-05`.', /** * @description Tooltip to explain why a cookie was blocked when the size of the #name plus the size of the value exceeds the max size. */ thisSetcookieWasBlockedBecauseTheNameValuePairExceedsMaxSize: 'This attempt to set a cookie via a `Set-Cookie` header was blocked because the cookie was too large. The combined size of the name and value must be less than or equal to 4096 characters.', /** * @description Text in Network Manager * @example {https://example.com} PH1 */ setcookieHeaderIsIgnoredIn: 'Set-Cookie header is ignored in response from url: {PH1}. The combined size of the name and value must be less than or equal to 4096 characters.', /** * @description Tooltip to explain why the cookie should have been blocked by third-party cookie phaseout but is exempted. */ exemptionReasonUserSetting: 'This cookie is allowed by user preference.', /** * @description Tooltip to explain why the cookie should have been blocked by third-party cookie phaseout but is exempted. */ exemptionReasonTPCDMetadata: 'This cookie is allowed by a third-party cookie deprecation trial grace period. Learn more: goo.gle/dt-grace.', /** * @description Tooltip to explain why the cookie should have been blocked by third-party cookie phaseout but is exempted. */ exemptionReasonTPCDDeprecationTrial: 'This cookie is allowed by third-party cookie deprecation trial. Learn more: goo.gle/ps-dt.', /** * @description Tooltip to explain why the cookie should have been blocked by third-party cookie phaseout but is exempted. */ exemptionReasonTopLevelTPCDDeprecationTrial: 'This cookie is allowed by top-level third-party cookie deprecation trial. Learn more: goo.gle/ps-dt.', /** * @description Tooltip to explain why the cookie should have been blocked by third-party cookie phaseout but is exempted. */ exemptionReasonTPCDHeuristics: 'This cookie is allowed by third-party cookie heuristics. Learn more: goo.gle/hbe', /** * @description Tooltip to explain why the cookie should have been blocked by third-party cookie phaseout but is exempted. */ exemptionReasonEnterprisePolicy: 'This cookie is allowed by Chrome Enterprise policy. Learn more: goo.gle/ce-3pc', /** * @description Tooltip to explain why the cookie should have been blocked by third-party cookie phaseout but is exempted. */ exemptionReasonStorageAccessAPI: 'This cookie is allowed by the Storage Access API. Learn more: goo.gle/saa', /** * @description Tooltip to explain why the cookie should have been blocked by third-party cookie phaseout but is exempted. */ exemptionReasonTopLevelStorageAccessAPI: 'This cookie is allowed by the top-level Storage Access API. Learn more: goo.gle/saa-top', /** * @description Tooltip to explain why the cookie should have been blocked by third-party cookie phaseout but is exempted. */ exemptionReasonScheme: 'This cookie is allowed by the top-level url scheme', } as const; // clang-format on const str_ = i18n.i18n.registerUIStrings('core/sdk/NetworkRequest.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class NetworkRequest extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements TextUtils.ContentProvider.StreamingContentProvider { #requestId: string; #backendRequestId?: Protocol.Network.RequestId; readonly #documentURL: Platform.DevToolsPath.UrlString; readonly #frameId: Protocol.Page.FrameId|null; readonly #loaderId: Protocol.Network.LoaderId|null; readonly #hasUserGesture: boolean|undefined; readonly #initiator: Protocol.Network.Initiator|null|undefined; #redirectSource: NetworkRequest|null = null; #preflightRequest: NetworkRequest|null = null; #preflightInitiatorRequest: NetworkRequest|null = null; #isRedirect = false; #redirectDestination: NetworkRequest|null = null; #issueTime = -1; #startTime = -1; #endTime = -1; #blockedReason: Protocol.Network.BlockedReason|undefined = undefined; #corsErrorStatus: Protocol.Network.CorsErrorStatus|undefined = undefined; statusCode = 0; statusText = ''; requestMethod = ''; requestTime = 0; protocol = ''; alternateProtocolUsage: Protocol.Network.AlternateProtocolUsage|undefined = undefined; mixedContentType: Protocol.Security.MixedContentType = Protocol.Security.MixedContentType.None; #initialPriority: Protocol.Network.ResourcePriority|null = null; #currentPriority: Protocol.Network.ResourcePriority|null = null; #signedExchangeInfo: Protocol.Network.SignedExchangeInfo|null = null; #webBundleInfo: WebBundleInfo|null = null; #webBundleInnerRequestInfo: WebBundleInnerRequestInfo|null = null; #resourceType: Common.ResourceType.ResourceType = Common.ResourceType.resourceTypes.Other; #contentData: Promise<TextUtils.ContentData.ContentDataOrError>|null = null; #streamingContentData: Promise<TextUtils.StreamingContentData.StreamingContentDataOrError>|null = null; readonly #frames: WebSocketFrame[] = []; #responseHeaderValues: Record<string, string|undefined> = {}; #responseHeadersText = ''; #originalResponseHeaders: Protocol.Fetch.HeaderEntry[] = []; #sortedOriginalResponseHeaders?: NameValue[]; // This field is only used when intercepting and overriding requests, because // in that case 'this.responseHeaders' does not contain 'set-cookie' headers. #setCookieHeaders: Protocol.Fetch.HeaderEntry[] = []; #requestHeaders: NameValue[] = []; #requestHeaderValues: Record<string, string|undefined> = {}; #remoteAddress = ''; #remoteAddressSpace: Protocol.Network.IPAddressSpace = Protocol.Network.IPAddressSpace.Unknown; #referrerPolicy: Protocol.Network.RequestReferrerPolicy|null = null; #securityState: Protocol.Security.SecurityState = Protocol.Security.SecurityState.Unknown; #securityDetails: Protocol.Network.SecurityDetails|null = null; connectionId = '0'; connectionReused = false; hasNetworkData = false; #formParametersPromise: Promise<NameValue[]|null>|null = null; #requestFormDataPromise: Promise<string|null>|null = Promise.resolve(null); #hasExtraRequestInfo = false; #hasExtraResponseInfo = false; #blockedRequestCookies: BlockedCookieWithReason[] = []; #includedRequestCookies: IncludedCookieWithReason[] = []; #blockedResponseCookies: BlockedSetCookieWithReason[] = []; #exemptedResponseCookies: ExemptedSetCookieWithReason[] = []; #responseCookiesPartitionKey: Protocol.Network.CookiePartitionKey|null = null; #responseCookiesPartitionKeyOpaque: boolean|null = null; #siteHasCookieInOtherPartition = false; localizedFailDescription: string|null = null; #url!: Platform.DevToolsPath.UrlString; #responseReceivedTime!: number; #transferSize!: number; #finished!: boolean; #failed!: boolean; #canceled!: boolean; #preserved!: boolean; #mimeType!: string; #charset!: string; #parsedURL!: Common.ParsedURL.ParsedURL; #name!: string|undefined; #path!: string|undefined; #clientSecurityState!:|Protocol.Network.ClientSecurityState|undefined; #trustTokenParams!: Protocol.Network.TrustTokenParams|undefined; #trustTokenOperationDoneEvent!:|Protocol.Network.TrustTokenOperationDoneEvent|undefined; #responseCacheStorageCacheName?: string; #serviceWorkerResponseSource?: Protocol.Network.ServiceWorkerResponseSource; #wallIssueTime?: number; #responseRetrievalTime?: Date; #resourceSize?: number; #fromMemoryCache?: boolean; #fromDiskCache?: boolean; #fromPrefetchCache?: boolean; #fromEarlyHints?: boolean; #fetchedViaServiceWorker?: boolean; #serviceWorkerRouterInfo?: Protocol.Network.ServiceWorkerRouterInfo; #timing?: Protocol.Network.ResourceTiming; #requestHeadersText?: string; #responseHeaders?: NameValue[]; #earlyHintsHeaders?: NameValue[]; #sortedResponseHeaders?: NameValue[]; #responseCookies?: Cookie[]; #serverTimings?: ServerTiming[]|null; #queryString?: string|null; #parsedQueryParameters?: NameValue[]; #contentDataProvider?: () => Promise<TextUtils.ContentData.ContentDataOrError>; #isSameSite: boolean|null = null; #wasIntercepted = false; #associatedData = new Map<string, object>(); #hasOverriddenContent = false; #hasThirdPartyCookiePhaseoutIssue = false; #serverSentEvents?: ServerSentEvents; responseReceivedPromise?: Promise<void>; responseReceivedPromiseResolve?: () => void; directSocketInfo?: DirectSocketInfo; readonly #directSocketChunks: DirectSocketChunk[] = []; #isIpProtectionUsed: boolean; constructor( requestId: string, backendRequestId: Protocol.Network.RequestId|undefined, url: Platform.DevToolsPath.UrlString, documentURL: Platform.DevToolsPath.UrlString, frameId: Protocol.Page.FrameId|null, loaderId: Protocol.Network.LoaderId|null, initiator: Protocol.Network.Initiator|null, hasUserGesture?: boolean, ) { super(); this.#requestId = requestId; this.#backendRequestId = backendRequestId; this.setUrl(url); this.#documentURL = documentURL; this.#frameId = frameId; this.#loaderId = loaderId; this.#initiator = initiator; this.#hasUserGesture = hasUserGesture; this.#isIpProtectionUsed = false; } static create( backendRequestId: Protocol.Network.RequestId, url: Platform.DevToolsPath.UrlString, documentURL: Platform.DevToolsPath.UrlString, frameId: Protocol.Page.FrameId|null, loaderId: Protocol.Network.LoaderId|null, initiator: Protocol.Network.Initiator|null, hasUserGesture?: boolean, ): NetworkRequest { return new NetworkRequest( backendRequestId, backendRequestId, url, documentURL, frameId, loaderId, initiator, hasUserGesture, ); } static createForSocket( backendRequestId: Protocol.Network.RequestId, requestURL: Platform.DevToolsPath.UrlString, initiator?: Protocol.Network.Initiator, ): NetworkRequest { return new NetworkRequest( backendRequestId, backendRequestId, requestURL, Platform.DevToolsPath.EmptyUrlString, null, null, initiator || null, ); } static createWithoutBackendRequest( requestId: string, url: Platform.DevToolsPath.UrlString, documentURL: Platform.DevToolsPath.UrlString, initiator: Protocol.Network.Initiator|null, ): NetworkRequest { return new NetworkRequest( requestId, undefined, url, documentURL, null, null, initiator, ); } identityCompare(other: NetworkRequest): number { const thisId = this.requestId(); const thatId = other.requestId(); if (thisId > thatId) { return 1; } if (thisId < thatId) { return -1; } return 0; } requestId(): string { return this.#requestId; } backendRequestId(): Protocol.Network.RequestId|undefined { return this.#backendRequestId; } url(): Platform.DevToolsPath.UrlString { return this.#url; } isBlobRequest(): boolean { return Common.ParsedURL.schemeIs(this.#url, 'blob:'); } setUrl(x: Platform.DevToolsPath.UrlString): void { if (this.#url === x) { return; } this.#url = x; this.#parsedURL = new Common.ParsedURL.ParsedURL(x); this.#queryString = undefined; this.#parsedQueryParameters = undefined; this.#name = undefined; this.#path = undefined; } get documentURL(): Platform.DevToolsPath.UrlString { return this.#documentURL; } get parsedURL(): Common.ParsedURL.ParsedURL { return this.#parsedURL; } get frameId(): Protocol.Page.FrameId|null { return this.#frameId; } get loaderId(): Protocol.Network.LoaderId|null { return this.#loaderId; } setRemoteAddress(ip: string, port: number): void { this.#remoteAddress = ip + ':' + port; this.dispatchEventToListeners(Events.REMOTE_ADDRESS_CHANGED, this); } remoteAddress(): string { return this.#remoteAddress; } remoteAddressSpace(): Protocol.Network.IPAddressSpace { return this.#remoteAddressSpace; } /** * The cache #name of the CacheStorage from where the response is served via * the ServiceWorker. */ getResponseCacheStorageCacheName(): string|undefined { return this.#responseCacheStorageCacheName; } setResponseCacheStorageCacheName(x: string): void { this.#responseCacheStorageCacheName = x; } serviceWorkerResponseSource():|Protocol.Network.ServiceWorkerResponseSource|undefined { return this.#serviceWorkerResponseSource; } setServiceWorkerResponseSource( serviceWorkerResponseSource: Protocol.Network.ServiceWorkerResponseSource, ): void { this.#serviceWorkerResponseSource = serviceWorkerResponseSource; } setReferrerPolicy( referrerPolicy: Protocol.Network.RequestReferrerPolicy, ): void { this.#referrerPolicy = referrerPolicy; } referrerPolicy(): Protocol.Network.RequestReferrerPolicy|null { return this.#referrerPolicy; } securityState(): Protocol.Security.SecurityState { return this.#securityState; } setSecurityState(securityState: Protocol.Security.SecurityState): void { this.#securityState = securityState; } securityDetails(): Protocol.Network.SecurityDetails|null { return this.#securityDetails; } securityOrigin(): string { return this.#parsedURL.securityOrigin(); } setSecurityDetails(securityDetails: Protocol.Network.SecurityDetails): void { this.#securityDetails = securityDetails; } get startTime(): number { return this.#startTime || -1; } setIssueTime(monotonicTime: number, wallTime: number): void { this.#issueTime = monotonicTime; this.#wallIssueTime = wallTime; this.#startTime = monotonicTime; } issueTime(): number { return this.#issueTime; } pseudoWallTime(monotonicTime: number): number { return this.#wallIssueTime ? this.#wallIssueTime - this.#issueTime + monotonicTime : monotonicTime; } get responseReceivedTime(): number { return this.#responseReceivedTime || -1; } set responseReceivedTime(x: number) { this.#responseReceivedTime = x; } /** * The time at which the returned response was generated. For cached * responses, this is the last time the cache entry was validated. */ getResponseRetrievalTime(): Date|undefined { return this.#responseRetrievalTime; } setResponseRetrievalTime(x: Date): void { this.#responseRetrievalTime = x; } get endTime(): number { return this.#endTime || -1; } set endTime(x: number) { if (this.timing?.requestTime) { // Check against accurate responseReceivedTime. this.#endTime = Math.max(x, this.responseReceivedTime); } else { // Prefer endTime since it might be from the network stack. this.#endTime = x; if (this.#responseReceivedTime > x) { this.#responseReceivedTime = x; } } this.dispatchEventToListeners(Events.TIMING_CHANGED, this); } get duration(): number { if (this.#endTime === -1 || this.#startTime === -1) { return -1; } return this.#endTime - this.#startTime; } get latency(): number { if (this.#responseReceivedTime === -1 || this.#startTime === -1) { return -1; } return this.#responseReceivedTime - this.#startTime; } get resourceSize(): number { return this.#resourceSize || 0; } set resourceSize(x: number) { this.#resourceSize = x; } get transferSize(): number { return this.#transferSize || 0; } increaseTransferSize(x: number): void { this.#transferSize = (this.#transferSize || 0) + x; } setTransferSize(x: number): void { this.#transferSize = x; } get finished(): boolean { return this.#finished; } set finished(x: boolean) { if (this.#finished === x) { return; } this.#finished = x; if (x) { this.dispatchEventToListeners(Events.FINISHED_LOADING, this); } } get failed(): boolean { return this.#failed; } set failed(x: boolean) { this.#failed = x; } get canceled(): boolean { return this.#canceled; } set canceled(x: boolean) { this.#canceled = x; } get preserved(): boolean { return this.#preserved; } set preserved(x: boolean) { this.#preserved = x; } blockedReason(): Protocol.Network.BlockedReason|undefined { return this.#blockedReason; } setBlockedReason(reason: Protocol.Network.BlockedReason): void { this.#blockedReason = reason; } corsErrorStatus(): Protocol.Network.CorsErrorStatus|undefined { return this.#corsErrorStatus; } setCorsErrorStatus(corsErrorStatus: Protocol.Network.CorsErrorStatus): void { this.#corsErrorStatus = corsErrorStatus; } wasBlocked(): boolean { return Boolean(this.#blockedReason); } cached(): boolean { return ((Boolean(this.#fromMemoryCache) || Boolean(this.#fromDiskCache)) && !this.#transferSize); } cachedInMemory(): boolean { return Boolean(this.#fromMemoryCache) && !this.#transferSize; } fromPrefetchCache(): boolean { return Boolean(this.#fromPrefetchCache); } setFromMemoryCache(): void { this.#fromMemoryCache = true; this.#timing = undefined; } get fromDiskCache(): boolean|undefined { return this.#fromDiskCache; } setFromDiskCache(): void { this.#fromDiskCache = true; } setFromPrefetchCache(): void { this.#fromPrefetchCache = true; } fromEarlyHints(): boolean { return Boolean(this.#fromEarlyHints); } setFromEarlyHints(): void { this.#fromEarlyHints = true; } /** * Returns true if the request was intercepted by a service worker and it * provided its own response. */ get fetchedViaServiceWorker(): boolean { return Boolean(this.#fetchedViaServiceWorker); } set fetchedViaServiceWorker(x: boolean) { this.#fetchedViaServiceWorker = x; } get serviceWorkerRouterInfo():|Protocol.Network.ServiceWorkerRouterInfo|undefined { return this.#serviceWorkerRouterInfo; } set serviceWorkerRouterInfo(x: Protocol.Network.ServiceWorkerRouterInfo) { this.#serviceWorkerRouterInfo = x; } /** * Returns true if the request was matched to a route when using the * ServiceWorker static routing API. */ hasMatchingServiceWorkerRouter(): boolean { // See definitions in `browser_protocol.pdl` for justification. return ( this.#serviceWorkerRouterInfo !== undefined && this.serviceWorkerRouterInfo?.matchedSourceType !== undefined); } /** * Returns true if the request was sent by a service worker. */ initiatedByServiceWorker(): boolean { const networkManager = NetworkManager.forRequest(this); if (!networkManager) { return false; } return networkManager.target().type() === Type.ServiceWorker; } get timing(): Protocol.Network.ResourceTiming|undefined { return this.#timing; } set timing(timingInfo: Protocol.Network.ResourceTiming|undefined) { if (!timingInfo || this.#fromMemoryCache) { return; } // Take startTime and responseReceivedTime from timing data for better accuracy. // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis. this.#startTime = timingInfo.requestTime; const headersReceivedTime = timingInfo.requestTime + timingInfo.receiveHeadersEnd / 1000.0; if ((this.#responseReceivedTime || -1) < 0 || this.#responseReceivedTime > headersReceivedTime) { this.#responseReceivedTime = headersReceivedTime; } if (this.#startTime > this.#responseReceivedTime) { this.#responseReceivedTime = this.#startTime; } this.#timing = timingInfo; this.dispatchEventToListeners(Events.TIMING_CHANGED, this); } private setConnectTimingFromExtraInfo( connectTiming: Protocol.Network.ConnectTiming, ): void { this.#startTime = connectTiming.requestTime; this.dispatchEventToListeners(Events.TIMING_CHANGED, this); } get mimeType(): string { return this.#mimeType; } set mimeType(x: string) { this.#mimeType = x; if (x === Platform.MimeType.MimeType.EVENTSTREAM && !this.#serverSentEvents) { const parseFromStreamedData = this.resourceType() !== Common.ResourceType.resourceTypes.EventSource; this.#serverSentEvents = new ServerSentEvents( this, parseFromStreamedData, ); } } get displayName(): string { return this.#parsedURL.displayName; } name(): string { if (this.#name) { return this.#name; } this.parseNameAndPathFromURL(); return this.#name as string; } path(): string { if (this.#path) { return this.#path; } this.parseNameAndPathFromURL(); return this.#path as string; } private parseNameAndPathFromURL(): void { if (this.#parsedURL.isDataURL()) { this.#name = this.#parsedURL.dataURLDisplayName(); this.#path = ''; } else if (this.#parsedURL.isBlobURL()) { this.#name = this.#parsedURL.url; this.#path = ''; } else if (this.#parsedURL.isAboutBlank()) { this.#name = this.#parsedURL.url; this.#path = ''; } else { this.#path = this.#parsedURL.host + this.#parsedURL.folderPathComponents; const networkManager = NetworkManager.forRequest(this); const inspectedURL = networkManager ? Common.ParsedURL.ParsedURL.fromString( networkManager.target().inspectedURL(), ) : null; this.#path = Platform.StringUtilities.trimURL( this.#path, inspectedURL ? inspectedURL.host : '', ); if (this.#parsedURL.lastPathComponent || this.#parsedURL.queryParams) { this.#name = this.#parsedURL.lastPathComponent + (this.#parsedURL.queryParams ? '?' + this.#parsedURL.queryParams : ''); } else if (this.#parsedURL.folderPathComponents) { this.#name = this.#parsedURL.folderPathComponents.substring( this.#parsedURL.folderPathComponents.lastIndexOf('/') + 1, ) + '/'; this.#path = this.#path.substring( 0, this.#path.lastIndexOf('/'), ); } else { this.#name = this.#parsedURL.host; this.#path = ''; } } } get folder(): string { let path: string = this.#parsedURL.path; const indexOfQuery = path.indexOf('?'); if (indexOfQuery !== -1) { path = path.substring(0, indexOfQuery); } const lastSlashIndex = path.lastIndexOf('/'); return lastSlashIndex !== -1 ? path.substring(0, lastSlashIndex) : ''; } get pathname(): string { return this.#parsedURL.path; } resourceType(): Common.ResourceType.ResourceType { return this.#resourceType; } setResourceType(resourceType: Common.ResourceType.ResourceType): void { this.#resourceType = resourceType; } get domain(): string { return this.#parsedURL.host; } get scheme(): string { return this.#parsedURL.scheme; } getInferredStatusText(): string { return (this.statusText || HttpReasonPhraseStrings.getStatusText(this.statusCode)); } redirectSource(): NetworkRequest|null { return this.#redirectSource; } setRedirectSource(originatingRequest: NetworkRequest|null): void { this.#redirectSource = originatingRequest; } preflightRequest(): NetworkRequest|null { return this.#preflightRequest; } setPreflightRequest(preflightRequest: NetworkRequest|null): void { this.#preflightRequest = preflightRequest; } preflightInitiatorRequest(): NetworkRequest|null { return this.#preflightInitiatorRequest; } setPreflightInitiatorRequest( preflightInitiatorRequest: NetworkRequest|null, ): void { this.#preflightInitiatorRequest = preflightInitiatorRequest; } isPreflightRequest(): boolean { return ( this.#initiator !== null && this.#initiator !== undefined && this.#initiator.type === Protocol.Network.InitiatorType.Preflight); } redirectDestination(): NetworkRequest|null { return this.#redirectDestination; } setRedirectDestination(redirectDestination: NetworkRequest|null): void { this.#redirectDestination = redirectDestination; } requestHeaders(): NameValue[] { return this.#requestHeaders; } setRequestHeaders(headers: NameValue[]): void { this.#requestHeaders = headers; this.dispatchEventToListeners(Events.REQUEST_HEADERS_CHANGED); } requestHeadersText(): string|undefined { return this.#requestHeadersText; } setRequestHeadersText(text: string): void { this.#requestHeadersText = text; this.dispatchEventToListeners(Events.REQUEST_HEADERS_CHANGED); } requestHeaderValue(headerName: string): string|undefined { if (this.#requestHeaderValues[headerName]) { return this.#requestHeaderValues[headerName]; } this.#requestHeaderValues[headerName] = this.computeHeaderValue( this.requestHeaders(), headerName, ); return this.#requestHeaderValues[headerName]; } requestFormData(): Promise<string|null> { if (!this.#requestFormDataPromise) { this.#requestFormDataPromise = NetworkManager.requestPostData(this); } return this.#requestFormDataPromise; } setRequestFormData(hasData: boolean, data: string|null): void { this.#requestFormDataPromise = hasData && data === null ? null : Promise.resolve(data); this.#formParametersPromise = null; } private filteredProtocolName(): string { const protocol = this.protocol.toLowerCase(); if (protocol === 'h2') { return 'http/2.0'; } return protocol.replace(/^http\/2(\.0)?\+/, 'http/2.0+'); } requestHttpVersion(): string { const headersText = this.requestHeadersText(); if (!headersText) { const version = this.requestHeaderValue('version') || this.requestHeaderValue(':version'); if (version) { return version; } return this.filteredProtocolName(); } const firstLine = headersText.split(/\r\n/)[0]; const match = firstLine.match(/(HTTP\/\d+\.\d+)$/); return match ? match[1] : 'HTTP/0.9'; } get responseHeaders(): NameValue[] { return this.#responseHeaders || []; } set responseHeaders(x: NameValue[]) { this.#responseHeaders = x; this.#sortedResponseHeaders = undefined; this.#serverTimings = undefined; this.#responseCookies = undefined; this.#responseHeaderValues = {}; this.dispatchEventToListeners(Events.RESPONSE_HEADERS_CHANGED); } get earlyHintsHeaders(): NameValue[] { return this.#earlyHintsHeaders || []; } set earlyHintsHeaders(x: NameValue[]) { this.#earlyHintsHeaders = x; } get originalResponseHeaders(): Protocol.Fetch.HeaderEntry[] { return this.#originalResponseHeaders; } set originalResponseHeaders(headers: Protocol.Fetch.HeaderEntry[]) { this.#originalResponseHeaders = headers; this.#sortedOriginalResponseHeaders = undefined; } get setCookieHeaders(): Protocol.Fetch.HeaderEntry[] { return this.#setCookieHeaders; } set setCookieHeaders(headers: Protocol.Fetch.HeaderEntry[]) { this.#setCookieHeaders = headers; } get responseHeadersText(): string { return this.#responseHeadersText; } set responseHeadersText(x: string) { this.#responseHeadersText = x; this.dispatchEventToListeners(Events.RESPONSE_HEADERS_CHANGED); } get sortedResponseHeaders(): NameValue[] { if (this.#sortedResponseHeaders !== undefined) { return this.#sortedResponseHeaders; } this.#sortedResponseHeaders = this.responseHeaders.slice(); return this.#sortedResponseHeaders.sort(function(a, b) { return Platform.StringUtilities.compare( a.name.toLowerCase(), b.name.toLowerCase(), ); }); } get sortedOriginalResponseHeaders(): NameValue[] { if (this.#sortedOriginalResponseHeaders !== undefined) { return this.#sortedOriginalResponseHeaders; } this.#sortedOriginalResponseHeaders = this.originalResponseHeaders.slice(); return this.#sortedOriginalResponseHeaders.sort(function(a, b) { return Platform.StringUtilities.compare( a.name.toLowerCase(), b.name.toLowerCase(), ); }); } get overrideTypes(): OverrideType[] { const types: OverrideType[] = []; if (this.hasOverriddenContent) { types.push('content'); } if (this.hasOverriddenHeaders()) { types.push('headers'); } return types; } get hasOverriddenContent(): boolean { return this.#hasOverriddenContent; } set hasOverriddenContent(value: boolean) { this.#hasOverriddenContent = value; } #deduplicateHeaders(sortedHeaders: NameValue[]): NameValue[] { const dedupedHeaders: NameValue[] = []; for (const header of sortedHeaders) { if (dedupedHeaders.length && dedupedHeaders[dedupedHeaders.length - 1].name === header.name) { dedupedHeaders[dedupedHeaders.length - 1].value += `, ${header.value}`; } else { dedupedHeaders.push({name: header.name, value: header.value}); } } return dedupedHeaders; } hasOverriddenHeaders(): boolean { if (!this.#originalResponseHeaders.length) { return false; } const responseHeaders = this.#deduplicateHeaders( this.sortedResponseHeaders, ); const originalResponseHeaders = this.#deduplicateHeaders( this.sortedOriginalResponseHeaders, ); if (responseHeaders.length !== originalResponseHeaders.length) { return true; } for (let i = 0; i < responseHeaders.length; i++) { if (responseHeaders[i].name.toLowerCase() !== originalResponseHeaders[i].name.toLowerCase()) { return true; } if (responseHeaders[i].value !== originalResponseHeaders[i].value) { return true; } } return false; } responseHeaderValue(headerName: string): string|undefined { if (headerName in this.#responseHeaderValues) { return this.#responseHeaderValues[headerName]; } this.#responseHeaderValues[headerName] = this.computeHeaderValue( this.responseHeaders, headerName, ); return this.#responseHeaderValues[headerName]; } wasIntercepted(): boolean { return this.#wasIntercepted; } setWasIntercepted(wasIntercepted: boolean): void { this.#wasIntercepted = wasIntercepted; } setEarlyHintsHeaders(headers: NameValue[]): void { this.earlyHintsHeaders = headers; } get responseCookies(): Cookie[] { if (!this.#responseCookies) { this.#responseCookies = CookieParser.parseSetCookie( this.responseHeaderValue('Set-Cookie'), this.domain, ) || []; if (this.#responseCookiesPartitionKey) { for (const cookie of this.#responseCookies) { if (cookie.partitioned()) { cookie.setPartitionKey( this.#responseCookiesPartitionKey.topLevelSite, this.#responseCookiesPartitionKey.hasCrossSiteAncestor, ); } } } else if (this.#responseCookiesPartitionKeyOpaque) { for (const cookie of this.#responseCookies) { // Do not check cookie.partitioned() since most opaque partitions // are fenced/credentialless frames partitioned by default. cookie.setPartitionKeyOpaque(); } } } return this.#responseCookies; } set responseCookies(responseCookies: Cookie[]) { this.#responseCookies = responseCookies; } responseLastModified(): string|undefined { return this.responseHeaderValue('last-modified'); } allCookiesIncludingBlockedOnes(): Cookie[] { return [ ...this.includedRequestCookies().map( includedRequestCookie => includedRequestCookie.cookie, ), ...this.responseCookies, ...this.blockedRequestCookies().map( blockedRequestCookie => blockedRequestCookie.cookie, ), ...this.blockedResponseCookies().map( blockedResponseCookie => blockedResponseCookie.cookie, ), ].filter(v => !!v); } get serverTimings(): ServerTiming[]|null { if (typeof this.#serverTimings === 'undefined') { this.#serverTimings = ServerTiming.parseHeaders( this.responseHeaders, ); } return this.#serverTimings; } queryString(): string|null { if (this.#queryString !== undefined) { return this.#queryString; } let queryString: string|null = null; const url = this.url(); const questionMarkPosition = url.indexOf('?'); if (questionMarkPosition !== -1) { queryString = url.substring(questionMarkPosition + 1); const hashSignPosition = queryString.indexOf('#'); if (hashSignPosition !== -1) { queryString = queryString.substring(0, hashSignPosition); } } this.#queryString = queryString; return this.#queryString; } get queryParameters(): NameValue[]|null { if (this.#parsedQueryParameters) { return this.#parsedQueryParameters; } const queryString = this.queryString(); if (!queryString) { return null; } this.#parsedQueryParameters = this.parseParameters(queryString); return this.#parsedQueryParameters; } private async parseFormParameters(): Promise<NameValue[]|null> { const requestContentType = this.requestContentType(); if (!requestContentType) { return null; } // Handling application/#x-www-form-urlencoded request bodies. if (requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) { const formData = await this.requestFormData(); if (!formData) { return null; } return this.parseParameters(formData); } // Handling multipart/form-data request bodies. const multipartDetails = requestContentType.match( /^multipart\/form-data\s*;\s*boundary\s*=\s*(\S+)\s*$/, ); if (!multipartDetails) { return null; } const boundary = multipartDetails[1]; if (!boundary) { return null; } const formData = await this.requestFormData(); if (!formData) { return null; } return this.parseMultipartFormDataParameters(formData, boundary); } formParameters(): Promise<NameValue[]|null> { if (!this.#formParametersPromise) { this.#formParametersPromise = this.parseFormParameters(); } return this.#formParametersPromise; } responseHttpVersion(): string { const headersText = this.#responseHeadersText; if (!headersText) { const version = this.responseHeaderValue('version') || this.responseHeaderValue(':version'); if (version) { return version; } return this.filteredProtocolName(); } const firstLine = headersText.split(/\r\n/)[0]; const match = firstLine.match(/^(HTTP\/\d+\.\d+)/); return match ? match[1] : 'HTTP/0.9'; } private parseParameters(queryString: string): NameValue[] { function parseNameValue(pair: string): {name: string, value: string} { const position = pair.indexOf('='); if (position === -1) { return {name: pair, value: ''}; } return { name: pair.substring(0, position), value: pair.substring(position + 1), }; } return queryString.split('&').map(parseNameValue); } /** * Parses multipart/form-data; boundary=boundaryString request bodies - * --boundaryString * Content-Disposition: form-data; #name="field-#name"; filename="r.gif" * Content-Type: application/octet-stream * * optionalValue * --boundaryString * Content-Disposition: form-data; #name="field-#name-2" * * optionalValue2 * --boundaryString-- */ private parseMultipartFormDataParameters( data: string, boundary: string, ): NameValue[] { const sanitizedBoundary = Platform.StringUtilities.escapeForRegExp(boundary); const keyValuePattern = new RegExp( // Header with an optional file #name. '^\\r\\ncontent-disposition\\s*:\\s*form-data\\s*;\\s*name="([^"]*)"(?:\\s*;\\s*filename="([^"]*)")?' + // Optional secondary header with the content type. '(?:\\r\\ncontent-type\\s*:\\s*([^\\r\\n]*))?' + // Padding. '\\r\\n\\r\\n' + // Value '(.*)' + // Padding. '\\r\\n$', 'is', ); const fields = data.split( new RegExp(`--${sanitizedBoundary}(?:--\s*$)?`, 'g'), ); return fields.reduce(parseMultipartField, []); function parseMultipartField( result: NameValue[], field: string, ): NameValue[] { const [match, name, filename, contentType, value] = field.match(keyValuePattern) || []; if (!match) { return result; } const processedValue = filename || contentType ? i18nString(UIStrings.binary) : value; result.push({name, value: processedValue}); return result; } } private computeHeaderValue( headers: NameValue[], headerName: string, ): string|undefined { headerName = headerName.toLowerCase(); const values = []; for (let i = 0; i < headers.length; ++i) { if (headers[i].name.toLowerCase() === headerName) { values.push(headers[i].value); } } if (!values.length) { return undefined; } // Set-Cookie #values should be separated by '\n', not comma, otherwise cookies could not be parsed. if (headerName === 'set-cookie') { return values.join('\n'); } return values.join(', '); } requestContentData(): Promise<TextUtils.ContentData.ContentDataOrError> { if (this.#contentData) { return this.#contentData; } if (this.#contentDataProvider) { this.#contentData = this.#contentDataProvider(); } else { this.#contentData = NetworkManager.requestContentData(this); } return this.#contentData; } setContentDataProvider( dataProvider: () => P