blob: bba616b12d848b7d3c0902f949a21ea4a2da0ac3 [file] [log] [blame]
Adrià Vilanova Martínez9c418ab2024-12-05 15:34:40 +01001import { XClientHeader, XClientValue } from '../../common/api';
2import {
3 correctArrayKeys,
4 inverseCorrectArrayKeys,
5} from '../../common/protojs';
6import { InterceptorHandlerPort } from '../interceptors/InterceptorHandler.port';
7import MessageIdTracker from '../MessageIdTracker';
8import { ResponseModifierPort } from '../ResponseModifier.port';
9import FetchBody from './FetchBody';
10import FetchHeaders from './FetchHeaders';
11import FetchInput from './FetchInput';
12
13export default class FetchProxyCallHandler {
14 private fetchHeaders: FetchHeaders;
15 private fetchBody: FetchBody;
16 private fetchInput: FetchInput;
17
18 private messageId: number;
19 private url: string;
20 private isArrayProto: boolean;
21
22 constructor(
23 private responseModifier: ResponseModifierPort,
24 private interceptorHandler: InterceptorHandlerPort,
25 private messageIdTracker: MessageIdTracker,
26 private originalFetch: typeof window.fetch,
27 ) {}
28
29 async proxiedFetch(
30 input: RequestInfo | URL,
31 init?: RequestInit,
32 ): Promise<Response> {
33 this.fetchHeaders = new FetchHeaders(init?.headers);
34 this.fetchBody = new FetchBody(init?.body);
35 this.fetchInput = new FetchInput(input);
36
37 const shouldIgnore = this.fetchHeaders.hasValue(
38 XClientHeader,
39 XClientValue,
40 );
41
42 // Remove the header after being read to preserve user privacy.
43 //
44 // If you're a Googler/TW team member reading this, and would like us to
45 // send this header to the server (e.g. for analytics purposes), please
46 // feel free to contact us (the community) at twpowertools-discuss [at]
47 // googlegroups.com!
48 this.fetchHeaders.removeHeader(XClientHeader);
49
50 if (shouldIgnore) {
51 return await this.originalFetch.apply(global, [input, init]);
52 }
53
54 this.messageId = this.messageIdTracker.getNewId();
55 this.url = this.fetchInput.getUrl();
56 this.isArrayProto = this.fetchHeaders.hasValue(
57 'Content-Type',
58 'application/json+protobuf',
59 );
60
61 await this.attemptToSendRequestInterceptorEvent();
62
63 const originalResponse: Response = await this.originalFetch.apply(global, [
64 input,
65 init,
66 ]);
67
68 const response = await this.attemptToModifyResponse(originalResponse);
69
70 await this.attemptToSendResponseInterceptorEvent(response);
71
72 return response;
73 }
74
75 private async attemptToSendRequestInterceptorEvent() {
76 try {
77 await this.sendRequestInterceptorEvent();
78 } catch (e) {
79 console.error(
80 `[FetchProxy] An error ocurred sending a request interceptor event for ${this.url}:`,
81 e,
82 );
83 }
84 }
85
86 private async sendRequestInterceptorEvent() {
87 const interceptors = this.interceptorHandler.matchInterceptors(
88 'request',
89 this.url,
90 );
91 if (interceptors.length === 0) return;
92
93 const rawBody = await this.fetchBody.getJSONRequestBody();
94 if (!rawBody) return;
95
96 const body = this.isArrayProto ? correctArrayKeys(rawBody) : rawBody;
97
98 for (const interceptor of interceptors) {
99 this.interceptorHandler.triggerEvent(
100 interceptor.eventName,
101 body,
102 this.messageId,
103 );
104 }
105 }
106
107 private async attemptToModifyResponse(
108 originalResponse: Response,
109 ): Promise<Response> {
110 try {
111 return await this.modifyResponse(originalResponse);
112 } catch (e) {
113 console.error(
114 `[Fetch Proxy] Couldn\'t modify the response for ${this.url}`,
115 e,
116 );
117 return originalResponse;
118 }
119 }
120
121 private async modifyResponse(originalResponse: Response) {
122 const response = originalResponse.clone();
123 let json = await response.json();
124
125 if (this.isArrayProto) {
126 correctArrayKeys(json);
127 }
128
129 const result = await this.responseModifier.intercept({
130 originalResponse: json,
131 url: this.url,
132 });
133 if (result.wasModified) {
134 json = result.modifiedResponse;
135 }
136
137 if (this.isArrayProto) {
138 inverseCorrectArrayKeys(json);
139 }
140
141 return new Response(JSON.stringify(json), {
142 status: response.status,
143 statusText: response.statusText,
144 headers: response.headers,
145 });
146 }
147
148 private async attemptToSendResponseInterceptorEvent(response: Response) {
149 try {
150 await this.sendResponseInterceptorEvent(response);
151 } catch (e) {
152 console.error(
153 `[FetchProxy] An error ocurred sending a response interceptor event for ${this.url}:`,
154 e,
155 );
156 }
157 }
158
159 private async sendResponseInterceptorEvent(response: Response) {
160 const interceptors = this.interceptorHandler.matchInterceptors(
161 'response',
162 this.url,
163 );
164 if (interceptors.length === 0) return;
165
166 const rawBody = await response.clone().json();
167 if (!rawBody) return;
168
169 const body = this.isArrayProto ? correctArrayKeys(rawBody) : rawBody;
170
171 for (const interceptor of interceptors) {
172 this.interceptorHandler.triggerEvent(
173 interceptor.eventName,
174 body,
175 this.messageId,
176 );
177 }
178 }
179}