From b1d19ac1a6734f91e408a69275c61bd3ca9007dc Mon Sep 17 00:00:00 2001 From: Brandon Duffany Date: Tue, 10 Oct 2023 17:32:55 -0400 Subject: [PATCH] Switch rpc_service to use Fetch API --- app/invocation/invocation_timing_card.tsx | 7 +- app/service/rpc_service.ts | 136 +++++++++++----------- 2 files changed, 75 insertions(+), 68 deletions(-) diff --git a/app/invocation/invocation_timing_card.tsx b/app/invocation/invocation_timing_card.tsx index 99400555cb7..a6afdca8a77 100644 --- a/app/invocation/invocation_timing_card.tsx +++ b/app/invocation/invocation_timing_card.tsx @@ -102,8 +102,11 @@ export default class InvocationTimingCardComponent extends React.Component this.updateProfile(parseProfile(contents))) + .fetchBytestreamFile(profileFile.uri, this.props.model.getInvocationId(), "text", { + // Set the stored encoding header to prevent the server from double-gzipping. + headers: { "X-Stored-Encoding-Hint": storedEncoding }, + }) + .then((contents) => this.updateProfile(parseProfile(contents))) .catch((e) => errorService.handleError(e)) .finally(() => this.setState({ loading: false })); } diff --git a/app/service/rpc_service.ts b/app/service/rpc_service.ts index 52d7b0e871c..adf7199db22 100644 --- a/app/service/rpc_service.ts +++ b/app/service/rpc_service.ts @@ -24,7 +24,7 @@ export type BuildBuddyServiceRpcName = RpcMethodNames( bytestreamURL: string, invocationId: string, - responseType?: XMLHttpResponseType, - { storedEncoding }: { storedEncoding?: FileEncoding } = {} - ) { - return this.fetchFile(this.getBytestreamUrl(bytestreamURL, invocationId), responseType || "", { - storedEncoding: storedEncoding, - }); + responseType?: T, + init: RequestInit = {} + ): Promise> { + return this.fetch( + this.getBytestreamUrl(bytestreamURL, invocationId), + (responseType || "") as FetchResponseType, + init + ) as Promise>; } - fetchFile( - fileURL: string, - responseType: XMLHttpResponseType, - { storedEncoding }: { storedEncoding?: FileEncoding } = {} - ): Promise { - return new Promise((resolve, reject) => { - var request = new XMLHttpRequest(); - request.responseType = responseType; - request.open("GET", fileURL, true); - request.onload = function () { - if (this.status >= 200 && this.status < 400) { - resolve(this.response); - } else { - let message: String; - if (this.response instanceof ArrayBuffer) { - message = new TextDecoder().decode(this.response); - } else { - message = String(this.response); - } - reject("Error loading file: " + message); - } - }; - request.onerror = function () { - reject("Error loading file (unknown error)"); - }; - // If we know the stored content is already gzipped, inform the server so - // that it doesn't double-gzip. - if (storedEncoding === "gzip") { - request.setRequestHeader("X-Stored-Encoding-Hint", "gzip"); - } else if (storedEncoding === "zstd") { - request.setRequestHeader("X-Stored-Encoding-Hint", "zstd"); - } - request.send(); - }); - } - - rpc(server: string, method: any, requestData: any, callback: any) { - var request = new XMLHttpRequest(); - request.open("POST", `${server || ""}/rpc/BuildBuddyService/${method.name}`, true); + /** + * Lowest-level fetch method. Ensures that tracing headers are set correctly, + * and handles returning the correct type of response based on the given + * response type. + */ + async fetch( + url: string, + responseType: T, + init: RequestInit = {} + ): Promise> { + const headers = new Headers(init.headers); if (this.debuggingEnabled()) { - request.setRequestHeader("x-buildbuddy-trace", "force"); + headers.set("x-buildbuddy-trace", "force"); } - if (capabilities.config.regions?.map((r) => r.server).includes(server)) { - request.withCredentials = true; + let response: Response; + try { + response = await fetch(url, { ...init, headers }); + } catch (e) { + throw `connection error: ${e}`; } - - request.setRequestHeader("Content-Type", method.contentType || "application/proto"); - request.responseType = "arraybuffer"; - request.onload = () => { - if (request.status >= 200 && request.status < 400) { - callback(null, new Uint8Array(request.response)); - this.events.next(method.name); - console.log(`Emitting event [${method.name}]`); - } else { - callback(new Error(`${new TextDecoder("utf-8").decode(new Uint8Array(request.response))}`)); + if (response.status < 200 || response.status >= 400) { + // Read error message from response body + let message = ""; + try { + message = await response.text(); + } catch (e) { + message = `unknown (failed to read response body: ${e})`; } - }; - - request.onerror = () => { - callback(new Error("Connection error")); - }; + throw `failed to fetch: ${message}`; + } + switch (responseType) { + case "arraybuffer": + return (await response.arrayBuffer()) as FetchPromiseType; + case "stream": + return response.body as FetchPromiseType; + default: + return (await response.text()) as FetchPromiseType; + } + } - request.send(requestData); + async rpc(server: string, method: any, requestData: any, callback: any) { + const url = `${server || ""}/rpc/BuildBuddyService/${method.name}`; + const init: RequestInit = { method: "POST", body: requestData }; + if (capabilities.config.regions?.map((r) => r.server).includes(server)) { + init.credentials = "include"; + } + init.headers = { "Content-Type": "application/proto" }; + try { + const arrayBuffer = await this.fetch(url, "arraybuffer", init); + callback(null, new Uint8Array(arrayBuffer)); + this.events.next(method.name); + } catch (e) { + console.error("RPC failed:", e); + callback(new Error(String(e))); + } } private getExtendedService(service: buildbuddy.service.BuildBuddyService): ExtendedBuildBuddyService { @@ -220,4 +214,14 @@ type CancelableService = protobufjs.rpc. : never; }; +type FetchPromiseType = T extends "" + ? string + : T extends "text" + ? string + : T extends "arraybuffer" + ? ArrayBuffer + : T extends "stream" + ? ReadableStream | null + : never; + export default new RpcService();