1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | import type {Protocol} from 'devtools-protocol';
|
8 |
|
9 | import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
|
10 | import type {Frame} from '../api/Frame.js';
|
11 | import type {Credentials} from '../api/Page.js';
|
12 | import {EventEmitter} from '../common/EventEmitter.js';
|
13 | import {
|
14 | NetworkManagerEvent,
|
15 | type NetworkManagerEvents,
|
16 | } from '../common/NetworkManagerEvents.js';
|
17 | import {debugError, isString} from '../common/util.js';
|
18 | import {assert} from '../util/assert.js';
|
19 | import {DisposableStack} from '../util/disposable.js';
|
20 |
|
21 | import {CdpHTTPRequest} from './HTTPRequest.js';
|
22 | import {CdpHTTPResponse} from './HTTPResponse.js';
|
23 | import {
|
24 | NetworkEventManager,
|
25 | type FetchRequestId,
|
26 | } from './NetworkEventManager.js';
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | export interface NetworkConditions {
|
32 | |
33 |
|
34 |
|
35 | download: number;
|
36 | |
37 |
|
38 |
|
39 | upload: number;
|
40 | |
41 |
|
42 |
|
43 | latency: number;
|
44 | }
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | export interface InternalNetworkConditions extends NetworkConditions {
|
50 | offline: boolean;
|
51 | }
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | export interface FrameProvider {
|
57 | frame(id: string): Frame | null;
|
58 | }
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | export class NetworkManager extends EventEmitter<NetworkManagerEvents> {
|
64 | #frameManager: FrameProvider;
|
65 | #networkEventManager = new NetworkEventManager();
|
66 | #extraHTTPHeaders?: Record<string, string>;
|
67 | #credentials: Credentials | null = null;
|
68 | #attemptedAuthentications = new Set<string>();
|
69 | #userRequestInterceptionEnabled = false;
|
70 | #protocolRequestInterceptionEnabled = false;
|
71 | #userCacheDisabled?: boolean;
|
72 | #emulatedNetworkConditions?: InternalNetworkConditions;
|
73 | #userAgent?: string;
|
74 | #userAgentMetadata?: Protocol.Emulation.UserAgentMetadata;
|
75 |
|
76 | readonly #handlers = [
|
77 | ['Fetch.requestPaused', this.#onRequestPaused],
|
78 | ['Fetch.authRequired', this.#onAuthRequired],
|
79 | ['Network.requestWillBeSent', this.#onRequestWillBeSent],
|
80 | ['Network.requestServedFromCache', this.#onRequestServedFromCache],
|
81 | ['Network.responseReceived', this.#onResponseReceived],
|
82 | ['Network.loadingFinished', this.#onLoadingFinished],
|
83 | ['Network.loadingFailed', this.#onLoadingFailed],
|
84 | ['Network.responseReceivedExtraInfo', this.#onResponseReceivedExtraInfo],
|
85 | [CDPSessionEvent.Disconnected, this.#removeClient],
|
86 | ] as const;
|
87 |
|
88 | #clients = new Map<CDPSession, DisposableStack>();
|
89 |
|
90 | constructor(frameManager: FrameProvider) {
|
91 | super();
|
92 | this.#frameManager = frameManager;
|
93 | }
|
94 |
|
95 | async addClient(client: CDPSession): Promise<void> {
|
96 | if (this.#clients.has(client)) {
|
97 | return;
|
98 | }
|
99 | const subscriptions = new DisposableStack();
|
100 | this.#clients.set(client, subscriptions);
|
101 | const clientEmitter = subscriptions.use(new EventEmitter(client));
|
102 |
|
103 | for (const [event, handler] of this.#handlers) {
|
104 | clientEmitter.on(event, (arg: any) => {
|
105 | return handler.bind(this)(client, arg);
|
106 | });
|
107 | }
|
108 |
|
109 | await Promise.all([
|
110 | client.send('Network.enable'),
|
111 | this.#applyExtraHTTPHeaders(client),
|
112 | this.#applyNetworkConditions(client),
|
113 | this.#applyProtocolCacheDisabled(client),
|
114 | this.#applyProtocolRequestInterception(client),
|
115 | this.#applyUserAgent(client),
|
116 | ]);
|
117 | }
|
118 |
|
119 | async #removeClient(client: CDPSession) {
|
120 | this.#clients.get(client)?.dispose();
|
121 | this.#clients.delete(client);
|
122 | }
|
123 |
|
124 | async authenticate(credentials: Credentials | null): Promise<void> {
|
125 | this.#credentials = credentials;
|
126 | const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
|
127 | if (enabled === this.#protocolRequestInterceptionEnabled) {
|
128 | return;
|
129 | }
|
130 | this.#protocolRequestInterceptionEnabled = enabled;
|
131 | await this.#applyToAllClients(
|
132 | this.#applyProtocolRequestInterception.bind(this),
|
133 | );
|
134 | }
|
135 |
|
136 | async setExtraHTTPHeaders(headers: Record<string, string>): Promise<void> {
|
137 | const extraHTTPHeaders: Record<string, string> = {};
|
138 | for (const [key, value] of Object.entries(headers)) {
|
139 | assert(
|
140 | isString(value),
|
141 | `Expected value of header "${key}" to be String, but "${typeof value}" is found.`,
|
142 | );
|
143 | extraHTTPHeaders[key.toLowerCase()] = value;
|
144 | }
|
145 | this.#extraHTTPHeaders = extraHTTPHeaders;
|
146 |
|
147 | await this.#applyToAllClients(this.#applyExtraHTTPHeaders.bind(this));
|
148 | }
|
149 |
|
150 | async #applyExtraHTTPHeaders(client: CDPSession) {
|
151 | if (this.#extraHTTPHeaders === undefined) {
|
152 | return;
|
153 | }
|
154 | await client.send('Network.setExtraHTTPHeaders', {
|
155 | headers: this.#extraHTTPHeaders,
|
156 | });
|
157 | }
|
158 |
|
159 | extraHTTPHeaders(): Record<string, string> {
|
160 | return Object.assign({}, this.#extraHTTPHeaders);
|
161 | }
|
162 |
|
163 | inFlightRequestsCount(): number {
|
164 | return this.#networkEventManager.inFlightRequestsCount();
|
165 | }
|
166 |
|
167 | async setOfflineMode(value: boolean): Promise<void> {
|
168 | if (!this.#emulatedNetworkConditions) {
|
169 | this.#emulatedNetworkConditions = {
|
170 | offline: false,
|
171 | upload: -1,
|
172 | download: -1,
|
173 | latency: 0,
|
174 | };
|
175 | }
|
176 | this.#emulatedNetworkConditions.offline = value;
|
177 | await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
|
178 | }
|
179 |
|
180 | async emulateNetworkConditions(
|
181 | networkConditions: NetworkConditions | null,
|
182 | ): Promise<void> {
|
183 | if (!this.#emulatedNetworkConditions) {
|
184 | this.#emulatedNetworkConditions = {
|
185 | offline: false,
|
186 | upload: -1,
|
187 | download: -1,
|
188 | latency: 0,
|
189 | };
|
190 | }
|
191 | this.#emulatedNetworkConditions.upload = networkConditions
|
192 | ? networkConditions.upload
|
193 | : -1;
|
194 | this.#emulatedNetworkConditions.download = networkConditions
|
195 | ? networkConditions.download
|
196 | : -1;
|
197 | this.#emulatedNetworkConditions.latency = networkConditions
|
198 | ? networkConditions.latency
|
199 | : 0;
|
200 |
|
201 | await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
|
202 | }
|
203 |
|
204 | async #applyToAllClients(fn: (client: CDPSession) => Promise<unknown>) {
|
205 | await Promise.all(
|
206 | Array.from(this.#clients.keys()).map(client => {
|
207 | return fn(client);
|
208 | }),
|
209 | );
|
210 | }
|
211 |
|
212 | async #applyNetworkConditions(client: CDPSession): Promise<void> {
|
213 | if (this.#emulatedNetworkConditions === undefined) {
|
214 | return;
|
215 | }
|
216 | await client.send('Network.emulateNetworkConditions', {
|
217 | offline: this.#emulatedNetworkConditions.offline,
|
218 | latency: this.#emulatedNetworkConditions.latency,
|
219 | uploadThroughput: this.#emulatedNetworkConditions.upload,
|
220 | downloadThroughput: this.#emulatedNetworkConditions.download,
|
221 | });
|
222 | }
|
223 |
|
224 | async setUserAgent(
|
225 | userAgent: string,
|
226 | userAgentMetadata?: Protocol.Emulation.UserAgentMetadata,
|
227 | ): Promise<void> {
|
228 | this.#userAgent = userAgent;
|
229 | this.#userAgentMetadata = userAgentMetadata;
|
230 | await this.#applyToAllClients(this.#applyUserAgent.bind(this));
|
231 | }
|
232 |
|
233 | async #applyUserAgent(client: CDPSession) {
|
234 | if (this.#userAgent === undefined) {
|
235 | return;
|
236 | }
|
237 | await client.send('Network.setUserAgentOverride', {
|
238 | userAgent: this.#userAgent,
|
239 | userAgentMetadata: this.#userAgentMetadata,
|
240 | });
|
241 | }
|
242 |
|
243 | async setCacheEnabled(enabled: boolean): Promise<void> {
|
244 | this.#userCacheDisabled = !enabled;
|
245 | await this.#applyToAllClients(this.#applyProtocolCacheDisabled.bind(this));
|
246 | }
|
247 |
|
248 | async setRequestInterception(value: boolean): Promise<void> {
|
249 | this.#userRequestInterceptionEnabled = value;
|
250 | const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
|
251 | if (enabled === this.#protocolRequestInterceptionEnabled) {
|
252 | return;
|
253 | }
|
254 | this.#protocolRequestInterceptionEnabled = enabled;
|
255 | await this.#applyToAllClients(
|
256 | this.#applyProtocolRequestInterception.bind(this),
|
257 | );
|
258 | }
|
259 |
|
260 | async #applyProtocolRequestInterception(client: CDPSession): Promise<void> {
|
261 | if (this.#userCacheDisabled === undefined) {
|
262 | this.#userCacheDisabled = false;
|
263 | }
|
264 | if (this.#protocolRequestInterceptionEnabled) {
|
265 | await Promise.all([
|
266 | this.#applyProtocolCacheDisabled(client),
|
267 | client.send('Fetch.enable', {
|
268 | handleAuthRequests: true,
|
269 | patterns: [{urlPattern: '*'}],
|
270 | }),
|
271 | ]);
|
272 | } else {
|
273 | await Promise.all([
|
274 | this.#applyProtocolCacheDisabled(client),
|
275 | client.send('Fetch.disable'),
|
276 | ]);
|
277 | }
|
278 | }
|
279 |
|
280 | async #applyProtocolCacheDisabled(client: CDPSession): Promise<void> {
|
281 | if (this.#userCacheDisabled === undefined) {
|
282 | return;
|
283 | }
|
284 | await client.send('Network.setCacheDisabled', {
|
285 | cacheDisabled: this.#userCacheDisabled,
|
286 | });
|
287 | }
|
288 |
|
289 | #onRequestWillBeSent(
|
290 | client: CDPSession,
|
291 | event: Protocol.Network.RequestWillBeSentEvent,
|
292 | ): void {
|
293 |
|
294 | if (
|
295 | this.#userRequestInterceptionEnabled &&
|
296 | !event.request.url.startsWith('data:')
|
297 | ) {
|
298 | const {requestId: networkRequestId} = event;
|
299 |
|
300 | this.#networkEventManager.storeRequestWillBeSent(networkRequestId, event);
|
301 |
|
302 | |
303 |
|
304 |
|
305 | const requestPausedEvent =
|
306 | this.#networkEventManager.getRequestPaused(networkRequestId);
|
307 | if (requestPausedEvent) {
|
308 | const {requestId: fetchRequestId} = requestPausedEvent;
|
309 | this.#patchRequestEventHeaders(event, requestPausedEvent);
|
310 | this.#onRequest(client, event, fetchRequestId);
|
311 | this.#networkEventManager.forgetRequestPaused(networkRequestId);
|
312 | }
|
313 |
|
314 | return;
|
315 | }
|
316 | this.#onRequest(client, event, undefined);
|
317 | }
|
318 |
|
319 | #onAuthRequired(
|
320 | client: CDPSession,
|
321 | event: Protocol.Fetch.AuthRequiredEvent,
|
322 | ): void {
|
323 | let response: Protocol.Fetch.AuthChallengeResponse['response'] = 'Default';
|
324 | if (this.#attemptedAuthentications.has(event.requestId)) {
|
325 | response = 'CancelAuth';
|
326 | } else if (this.#credentials) {
|
327 | response = 'ProvideCredentials';
|
328 | this.#attemptedAuthentications.add(event.requestId);
|
329 | }
|
330 | const {username, password} = this.#credentials || {
|
331 | username: undefined,
|
332 | password: undefined,
|
333 | };
|
334 | client
|
335 | .send('Fetch.continueWithAuth', {
|
336 | requestId: event.requestId,
|
337 | authChallengeResponse: {response, username, password},
|
338 | })
|
339 | .catch(debugError);
|
340 | }
|
341 |
|
342 | |
343 |
|
344 |
|
345 |
|
346 |
|
347 |
|
348 |
|
349 | #onRequestPaused(
|
350 | client: CDPSession,
|
351 | event: Protocol.Fetch.RequestPausedEvent,
|
352 | ): void {
|
353 | if (
|
354 | !this.#userRequestInterceptionEnabled &&
|
355 | this.#protocolRequestInterceptionEnabled
|
356 | ) {
|
357 | client
|
358 | .send('Fetch.continueRequest', {
|
359 | requestId: event.requestId,
|
360 | })
|
361 | .catch(debugError);
|
362 | }
|
363 |
|
364 | const {networkId: networkRequestId, requestId: fetchRequestId} = event;
|
365 |
|
366 | if (!networkRequestId) {
|
367 | this.#onRequestWithoutNetworkInstrumentation(client, event);
|
368 | return;
|
369 | }
|
370 |
|
371 | const requestWillBeSentEvent = (() => {
|
372 | const requestWillBeSentEvent =
|
373 | this.#networkEventManager.getRequestWillBeSent(networkRequestId);
|
374 |
|
375 |
|
376 | if (
|
377 | requestWillBeSentEvent &&
|
378 | (requestWillBeSentEvent.request.url !== event.request.url ||
|
379 | requestWillBeSentEvent.request.method !== event.request.method)
|
380 | ) {
|
381 | this.#networkEventManager.forgetRequestWillBeSent(networkRequestId);
|
382 | return;
|
383 | }
|
384 | return requestWillBeSentEvent;
|
385 | })();
|
386 |
|
387 | if (requestWillBeSentEvent) {
|
388 | this.#patchRequestEventHeaders(requestWillBeSentEvent, event);
|
389 | this.#onRequest(client, requestWillBeSentEvent, fetchRequestId);
|
390 | } else {
|
391 | this.#networkEventManager.storeRequestPaused(networkRequestId, event);
|
392 | }
|
393 | }
|
394 |
|
395 | #patchRequestEventHeaders(
|
396 | requestWillBeSentEvent: Protocol.Network.RequestWillBeSentEvent,
|
397 | requestPausedEvent: Protocol.Fetch.RequestPausedEvent,
|
398 | ): void {
|
399 | requestWillBeSentEvent.request.headers = {
|
400 | ...requestWillBeSentEvent.request.headers,
|
401 | // includes extra headers, like: Accept, Origin
|
402 | ...requestPausedEvent.request.headers,
|
403 | };
|
404 | }
|
405 |
|
406 | #onRequestWithoutNetworkInstrumentation(
|
407 | client: CDPSession,
|
408 | event: Protocol.Fetch.RequestPausedEvent,
|
409 | ): void {
|
410 | // If an event has no networkId it should not have any network events. We
|
411 | // still want to dispatch it for the interception by the user.
|
412 | const frame = event.frameId
|
413 | ? this.#frameManager.frame(event.frameId)
|
414 | : null;
|
415 |
|
416 | const request = new CdpHTTPRequest(
|
417 | client,
|
418 | frame,
|
419 | event.requestId,
|
420 | this.#userRequestInterceptionEnabled,
|
421 | event,
|
422 | [],
|
423 | );
|
424 | this.emit(NetworkManagerEvent.Request, request);
|
425 | void request.finalizeInterceptions();
|
426 | }
|
427 |
|
428 | #onRequest(
|
429 | client: CDPSession,
|
430 | event: Protocol.Network.RequestWillBeSentEvent,
|
431 | fetchRequestId?: FetchRequestId,
|
432 | fromMemoryCache = false,
|
433 | ): void {
|
434 | let redirectChain: CdpHTTPRequest[] = [];
|
435 | if (event.redirectResponse) {
|
436 | // We want to emit a response and requestfinished for the
|
437 | // redirectResponse, but we can't do so unless we have a
|
438 | // responseExtraInfo ready to pair it up with. If we don't have any
|
439 | // responseExtraInfos saved in our queue, they we have to wait until
|
440 | // the next one to emit response and requestfinished, *and* we should
|
441 | // also wait to emit this Request too because it should come after the
|
442 | // response/requestfinished.
|
443 | let redirectResponseExtraInfo = null;
|
444 | if (event.redirectHasExtraInfo) {
|
445 | redirectResponseExtraInfo = this.#networkEventManager
|
446 | .responseExtraInfo(event.requestId)
|
447 | .shift();
|
448 | if (!redirectResponseExtraInfo) {
|
449 | this.#networkEventManager.queueRedirectInfo(event.requestId, {
|
450 | event,
|
451 | fetchRequestId,
|
452 | });
|
453 | return;
|
454 | }
|
455 | }
|
456 |
|
457 | const request = this.#networkEventManager.getRequest(event.requestId);
|
458 | // If we connect late to the target, we could have missed the
|
459 | // requestWillBeSent event.
|
460 | if (request) {
|
461 | this.#handleRequestRedirect(
|
462 | client,
|
463 | request,
|
464 | event.redirectResponse,
|
465 | redirectResponseExtraInfo,
|
466 | );
|
467 | redirectChain = request._redirectChain;
|
468 | }
|
469 | }
|
470 | const frame = event.frameId
|
471 | ? this.#frameManager.frame(event.frameId)
|
472 | : null;
|
473 |
|
474 | const request = new CdpHTTPRequest(
|
475 | client,
|
476 | frame,
|
477 | fetchRequestId,
|
478 | this.#userRequestInterceptionEnabled,
|
479 | event,
|
480 | redirectChain,
|
481 | );
|
482 | request._fromMemoryCache = fromMemoryCache;
|
483 | this.#networkEventManager.storeRequest(event.requestId, request);
|
484 | this.emit(NetworkManagerEvent.Request, request);
|
485 | void request.finalizeInterceptions();
|
486 | }
|
487 |
|
488 | #onRequestServedFromCache(
|
489 | client: CDPSession,
|
490 | event: Protocol.Network.RequestServedFromCacheEvent,
|
491 | ): void {
|
492 | const requestWillBeSentEvent =
|
493 | this.#networkEventManager.getRequestWillBeSent(event.requestId);
|
494 | let request = this.#networkEventManager.getRequest(event.requestId);
|
495 | // Requests served from memory cannot be intercepted.
|
496 | if (request) {
|
497 | request._fromMemoryCache = true;
|
498 | }
|
499 | // If request ended up being served from cache, we need to convert
|
500 | // requestWillBeSentEvent to a HTTP request.
|
501 | if (!request && requestWillBeSentEvent) {
|
502 | this.#onRequest(client, requestWillBeSentEvent, undefined, true);
|
503 | request = this.#networkEventManager.getRequest(event.requestId);
|
504 | }
|
505 | if (!request) {
|
506 | debugError(
|
507 | new Error(
|
508 | `Request ${event.requestId} was served from cache but we could not find the corresponding request object`,
|
509 | ),
|
510 | );
|
511 | return;
|
512 | }
|
513 | this.emit(NetworkManagerEvent.RequestServedFromCache, request);
|
514 | }
|
515 |
|
516 | #handleRequestRedirect(
|
517 | _client: CDPSession,
|
518 | request: CdpHTTPRequest,
|
519 | responsePayload: Protocol.Network.Response,
|
520 | extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
|
521 | ): void {
|
522 | const response = new CdpHTTPResponse(request, responsePayload, extraInfo);
|
523 | request._response = response;
|
524 | request._redirectChain.push(request);
|
525 | response._resolveBody(
|
526 | new Error('Response body is unavailable for redirect responses'),
|
527 | );
|
528 | this.#forgetRequest(request, false);
|
529 | this.emit(NetworkManagerEvent.Response, response);
|
530 | this.emit(NetworkManagerEvent.RequestFinished, request);
|
531 | }
|
532 |
|
533 | #emitResponseEvent(
|
534 | _client: CDPSession,
|
535 | responseReceived: Protocol.Network.ResponseReceivedEvent,
|
536 | extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
|
537 | ): void {
|
538 | const request = this.#networkEventManager.getRequest(
|
539 | responseReceived.requestId,
|
540 | );
|
541 | // FileUpload sends a response without a matching request.
|
542 | if (!request) {
|
543 | return;
|
544 | }
|
545 |
|
546 | const extraInfos = this.#networkEventManager.responseExtraInfo(
|
547 | responseReceived.requestId,
|
548 | );
|
549 | if (extraInfos.length) {
|
550 | debugError(
|
551 | new Error(
|
552 | 'Unexpected extraInfo events for request ' +
|
553 | responseReceived.requestId,
|
554 | ),
|
555 | );
|
556 | }
|
557 |
|
558 | // Chromium sends wrong extraInfo events for responses served from cache.
|
559 | // See https://github.com/puppeteer/puppeteer/issues/9965 and
|
560 | // https://crbug.com/1340398.
|
561 | if (responseReceived.response.fromDiskCache) {
|
562 | extraInfo = null;
|
563 | }
|
564 |
|
565 | const response = new CdpHTTPResponse(
|
566 | request,
|
567 | responseReceived.response,
|
568 | extraInfo,
|
569 | );
|
570 | request._response = response;
|
571 | this.emit(NetworkManagerEvent.Response, response);
|
572 | }
|
573 |
|
574 | #onResponseReceived(
|
575 | client: CDPSession,
|
576 | event: Protocol.Network.ResponseReceivedEvent,
|
577 | ): void {
|
578 | const request = this.#networkEventManager.getRequest(event.requestId);
|
579 | let extraInfo = null;
|
580 | if (request && !request._fromMemoryCache && event.hasExtraInfo) {
|
581 | extraInfo = this.#networkEventManager
|
582 | .responseExtraInfo(event.requestId)
|
583 | .shift();
|
584 | if (!extraInfo) {
|
585 | // Wait until we get the corresponding ExtraInfo event.
|
586 | this.#networkEventManager.queueEventGroup(event.requestId, {
|
587 | responseReceivedEvent: event,
|
588 | });
|
589 | return;
|
590 | }
|
591 | }
|
592 | this.#emitResponseEvent(client, event, extraInfo);
|
593 | }
|
594 |
|
595 | #onResponseReceivedExtraInfo(
|
596 | client: CDPSession,
|
597 | event: Protocol.Network.ResponseReceivedExtraInfoEvent,
|
598 | ): void {
|
599 | // We may have skipped a redirect response/request pair due to waiting for
|
600 | // this ExtraInfo event. If so, continue that work now that we have the
|
601 | // request.
|
602 | const redirectInfo = this.#networkEventManager.takeQueuedRedirectInfo(
|
603 | event.requestId,
|
604 | );
|
605 | if (redirectInfo) {
|
606 | this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
|
607 | this.#onRequest(client, redirectInfo.event, redirectInfo.fetchRequestId);
|
608 | return;
|
609 | }
|
610 |
|
611 | // We may have skipped response and loading events because we didn't have
|
612 | // this ExtraInfo event yet. If so, emit those events now.
|
613 | const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
|
614 | event.requestId,
|
615 | );
|
616 | if (queuedEvents) {
|
617 | this.#networkEventManager.forgetQueuedEventGroup(event.requestId);
|
618 | this.#emitResponseEvent(
|
619 | client,
|
620 | queuedEvents.responseReceivedEvent,
|
621 | event,
|
622 | );
|
623 | if (queuedEvents.loadingFinishedEvent) {
|
624 | this.#emitLoadingFinished(client, queuedEvents.loadingFinishedEvent);
|
625 | }
|
626 | if (queuedEvents.loadingFailedEvent) {
|
627 | this.#emitLoadingFailed(client, queuedEvents.loadingFailedEvent);
|
628 | }
|
629 | return;
|
630 | }
|
631 |
|
632 | // Wait until we get another event that can use this ExtraInfo event.
|
633 | this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
|
634 | }
|
635 |
|
636 | #forgetRequest(request: CdpHTTPRequest, events: boolean): void {
|
637 | const requestId = request.id;
|
638 | const interceptionId = request._interceptionId;
|
639 |
|
640 | this.#networkEventManager.forgetRequest(requestId);
|
641 | if (interceptionId !== undefined) {
|
642 | this.#attemptedAuthentications.delete(interceptionId);
|
643 | }
|
644 |
|
645 | if (events) {
|
646 | this.#networkEventManager.forget(requestId);
|
647 | }
|
648 | }
|
649 |
|
650 | #onLoadingFinished(
|
651 | client: CDPSession,
|
652 | event: Protocol.Network.LoadingFinishedEvent,
|
653 | ): void {
|
654 | // If the response event for this request is still waiting on a
|
655 | // corresponding ExtraInfo event, then wait to emit this event too.
|
656 | const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
|
657 | event.requestId,
|
658 | );
|
659 | if (queuedEvents) {
|
660 | queuedEvents.loadingFinishedEvent = event;
|
661 | } else {
|
662 | this.#emitLoadingFinished(client, event);
|
663 | }
|
664 | }
|
665 |
|
666 | #emitLoadingFinished(
|
667 | client: CDPSession,
|
668 | event: Protocol.Network.LoadingFinishedEvent,
|
669 | ): void {
|
670 | const request = this.#networkEventManager.getRequest(event.requestId);
|
671 | // For certain requestIds we never receive requestWillBeSent event.
|
672 | // @see https://crbug.com/750469
|
673 | if (!request) {
|
674 | return;
|
675 | }
|
676 |
|
677 | this.#maybeReassignOOPIFRequestClient(client, request);
|
678 |
|
679 | // Under certain conditions we never get the Network.responseReceived
|
680 | // event from protocol. @see https://crbug.com/883475
|
681 | if (request.response()) {
|
682 | request.response()?._resolveBody();
|
683 | }
|
684 | this.#forgetRequest(request, true);
|
685 | this.emit(NetworkManagerEvent.RequestFinished, request);
|
686 | }
|
687 |
|
688 | #onLoadingFailed(
|
689 | client: CDPSession,
|
690 | event: Protocol.Network.LoadingFailedEvent,
|
691 | ): void {
|
692 | // If the response event for this request is still waiting on a
|
693 | // corresponding ExtraInfo event, then wait to emit this event too.
|
694 | const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
|
695 | event.requestId,
|
696 | );
|
697 | if (queuedEvents) {
|
698 | queuedEvents.loadingFailedEvent = event;
|
699 | } else {
|
700 | this.#emitLoadingFailed(client, event);
|
701 | }
|
702 | }
|
703 |
|
704 | #emitLoadingFailed(
|
705 | client: CDPSession,
|
706 | event: Protocol.Network.LoadingFailedEvent,
|
707 | ): void {
|
708 | const request = this.#networkEventManager.getRequest(event.requestId);
|
709 | // For certain requestIds we never receive requestWillBeSent event.
|
710 | // @see https://crbug.com/750469
|
711 | if (!request) {
|
712 | return;
|
713 | }
|
714 | this.#maybeReassignOOPIFRequestClient(client, request);
|
715 | request._failureText = event.errorText;
|
716 | const response = request.response();
|
717 | if (response) {
|
718 | response._resolveBody();
|
719 | }
|
720 | this.#forgetRequest(request, true);
|
721 | this.emit(NetworkManagerEvent.RequestFailed, request);
|
722 | }
|
723 |
|
724 | #maybeReassignOOPIFRequestClient(
|
725 | client: CDPSession,
|
726 | request: CdpHTTPRequest,
|
727 | ): void {
|
728 | // Document requests for OOPIFs start in the parent frame but are adopted by their
|
729 | // child frame, meaning their loadingFinished and loadingFailed events are fired on
|
730 | // the child session. In this case we reassign the request CDPSession to ensure all
|
731 | // subsequent actions use the correct session (e.g. retrieving response body in
|
732 |
|
733 | if (client !== request.client && request.isNavigationRequest()) {
|
734 | request.client = client;
|
735 | }
|
736 | }
|
737 | }
|
738 |
|
\ | No newline at end of file |