UNPKG

23.2 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright 2017 Google Inc.
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7import type {Protocol} from 'devtools-protocol';
8
9import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
10import type {Frame} from '../api/Frame.js';
11import type {Credentials} from '../api/Page.js';
12import {EventEmitter} from '../common/EventEmitter.js';
13import {
14 NetworkManagerEvent,
15 type NetworkManagerEvents,
16} from '../common/NetworkManagerEvents.js';
17import {debugError, isString} from '../common/util.js';
18import {assert} from '../util/assert.js';
19import {DisposableStack} from '../util/disposable.js';
20
21import {CdpHTTPRequest} from './HTTPRequest.js';
22import {CdpHTTPResponse} from './HTTPResponse.js';
23import {
24 NetworkEventManager,
25 type FetchRequestId,
26} from './NetworkEventManager.js';
27
28/**
29 * @public
30 */
31export interface NetworkConditions {
32 /**
33 * Download speed (bytes/s)
34 */
35 download: number;
36 /**
37 * Upload speed (bytes/s)
38 */
39 upload: number;
40 /**
41 * Latency (ms)
42 */
43 latency: number;
44}
45
46/**
47 * @public
48 */
49export interface InternalNetworkConditions extends NetworkConditions {
50 offline: boolean;
51}
52
53/**
54 * @internal
55 */
56export interface FrameProvider {
57 frame(id: string): Frame | null;
58}
59
60/**
61 * @internal
62 */
63export 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 // Request interception doesn't happen for data URLs with Network Service.
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 * CDP may have sent a Fetch.requestPaused event already. Check for it.
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 * CDP may send a Fetch.requestPaused without or before a
344 * Network.requestWillBeSent
345 *
346 * CDP may send multiple Fetch.requestPaused
347 * for the same Network.requestWillBeSent.
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 // redirect requests have the same `requestId`,
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 // HTTPResponse).
733 if (client !== request.client && request.isNavigationRequest()) {
734 request.client = client;
735 }
736 }
737}
738
\No newline at end of file