fix: add FetchProxy
This will let us intercept fetch requests (until now we're only proxying
XMLHttpRequest), in order to fix the issues we're experiencing with some
features.
Bug: twpowertools:229
Change-Id: I277473c05479ca39bb6183a51855382124890bde
diff --git a/src/xhrInterceptor/fetchProxy/FetchProxyCallHandler.ts b/src/xhrInterceptor/fetchProxy/FetchProxyCallHandler.ts
new file mode 100644
index 0000000..bba616b
--- /dev/null
+++ b/src/xhrInterceptor/fetchProxy/FetchProxyCallHandler.ts
@@ -0,0 +1,179 @@
+import { XClientHeader, XClientValue } from '../../common/api';
+import {
+ correctArrayKeys,
+ inverseCorrectArrayKeys,
+} from '../../common/protojs';
+import { InterceptorHandlerPort } from '../interceptors/InterceptorHandler.port';
+import MessageIdTracker from '../MessageIdTracker';
+import { ResponseModifierPort } from '../ResponseModifier.port';
+import FetchBody from './FetchBody';
+import FetchHeaders from './FetchHeaders';
+import FetchInput from './FetchInput';
+
+export default class FetchProxyCallHandler {
+ private fetchHeaders: FetchHeaders;
+ private fetchBody: FetchBody;
+ private fetchInput: FetchInput;
+
+ private messageId: number;
+ private url: string;
+ private isArrayProto: boolean;
+
+ constructor(
+ private responseModifier: ResponseModifierPort,
+ private interceptorHandler: InterceptorHandlerPort,
+ private messageIdTracker: MessageIdTracker,
+ private originalFetch: typeof window.fetch,
+ ) {}
+
+ async proxiedFetch(
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ): Promise<Response> {
+ this.fetchHeaders = new FetchHeaders(init?.headers);
+ this.fetchBody = new FetchBody(init?.body);
+ this.fetchInput = new FetchInput(input);
+
+ const shouldIgnore = this.fetchHeaders.hasValue(
+ XClientHeader,
+ XClientValue,
+ );
+
+ // Remove the header after being read to preserve user privacy.
+ //
+ // If you're a Googler/TW team member reading this, and would like us to
+ // send this header to the server (e.g. for analytics purposes), please
+ // feel free to contact us (the community) at twpowertools-discuss [at]
+ // googlegroups.com!
+ this.fetchHeaders.removeHeader(XClientHeader);
+
+ if (shouldIgnore) {
+ return await this.originalFetch.apply(global, [input, init]);
+ }
+
+ this.messageId = this.messageIdTracker.getNewId();
+ this.url = this.fetchInput.getUrl();
+ this.isArrayProto = this.fetchHeaders.hasValue(
+ 'Content-Type',
+ 'application/json+protobuf',
+ );
+
+ await this.attemptToSendRequestInterceptorEvent();
+
+ const originalResponse: Response = await this.originalFetch.apply(global, [
+ input,
+ init,
+ ]);
+
+ const response = await this.attemptToModifyResponse(originalResponse);
+
+ await this.attemptToSendResponseInterceptorEvent(response);
+
+ return response;
+ }
+
+ private async attemptToSendRequestInterceptorEvent() {
+ try {
+ await this.sendRequestInterceptorEvent();
+ } catch (e) {
+ console.error(
+ `[FetchProxy] An error ocurred sending a request interceptor event for ${this.url}:`,
+ e,
+ );
+ }
+ }
+
+ private async sendRequestInterceptorEvent() {
+ const interceptors = this.interceptorHandler.matchInterceptors(
+ 'request',
+ this.url,
+ );
+ if (interceptors.length === 0) return;
+
+ const rawBody = await this.fetchBody.getJSONRequestBody();
+ if (!rawBody) return;
+
+ const body = this.isArrayProto ? correctArrayKeys(rawBody) : rawBody;
+
+ for (const interceptor of interceptors) {
+ this.interceptorHandler.triggerEvent(
+ interceptor.eventName,
+ body,
+ this.messageId,
+ );
+ }
+ }
+
+ private async attemptToModifyResponse(
+ originalResponse: Response,
+ ): Promise<Response> {
+ try {
+ return await this.modifyResponse(originalResponse);
+ } catch (e) {
+ console.error(
+ `[Fetch Proxy] Couldn\'t modify the response for ${this.url}`,
+ e,
+ );
+ return originalResponse;
+ }
+ }
+
+ private async modifyResponse(originalResponse: Response) {
+ const response = originalResponse.clone();
+ let json = await response.json();
+
+ if (this.isArrayProto) {
+ correctArrayKeys(json);
+ }
+
+ const result = await this.responseModifier.intercept({
+ originalResponse: json,
+ url: this.url,
+ });
+ if (result.wasModified) {
+ json = result.modifiedResponse;
+ }
+
+ if (this.isArrayProto) {
+ inverseCorrectArrayKeys(json);
+ }
+
+ return new Response(JSON.stringify(json), {
+ status: response.status,
+ statusText: response.statusText,
+ headers: response.headers,
+ });
+ }
+
+ private async attemptToSendResponseInterceptorEvent(response: Response) {
+ try {
+ await this.sendResponseInterceptorEvent(response);
+ } catch (e) {
+ console.error(
+ `[FetchProxy] An error ocurred sending a response interceptor event for ${this.url}:`,
+ e,
+ );
+ }
+ }
+
+ private async sendResponseInterceptorEvent(response: Response) {
+ const interceptors = this.interceptorHandler.matchInterceptors(
+ 'response',
+ this.url,
+ );
+ if (interceptors.length === 0) return;
+
+ const rawBody = await response.clone().json();
+ if (!rawBody) return;
+
+ const body = this.isArrayProto ? correctArrayKeys(rawBody) : rawBody;
+
+ for (const interceptor of interceptors) {
+ this.interceptorHandler.triggerEvent(
+ interceptor.eventName,
+ body,
+ this.messageId,
+ );
+ }
+ }
+}