go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/frontend/ui/src/services/prpc_client.ts (about) 1 // Copyright 2024 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Import the error and RPC code types from '@chopsui/prpc-client' so we can 16 // handle the errors from the binary client and the original client the same 17 // way. 18 // 19 // TODO(crbug/1504937): drop the '@chopsui/prpc-client' and declare our own 20 // error and RPC code types once all other usage of prpc-client is migrated to 21 // the binary client. 22 import { GrpcError, ProtocolError, RpcCode } from '@chopsui/prpc-client'; 23 24 export interface PrpcClientOptions { 25 /** 26 * pRPC server host, defaults to current document host. 27 */ 28 readonly host?: string; 29 /** 30 * Auth token to use in RPC. Defaults to `() => ''`. 31 */ 32 readonly getAuthToken?: () => string | Promise<string>; 33 /** 34 * If true, use HTTP instead of HTTPS. Defaults to `false`. 35 */ 36 readonly insecure?: boolean; 37 /** 38 * If supplied, use this function instead of fetch. 39 */ 40 readonly fetchImpl?: typeof fetch; 41 } 42 43 /** 44 * Class for interacting with a pRPC API with a JSON protocol. 45 * Protocol: https://godoc.org/go.chromium.org/luci/grpc/prpc 46 */ 47 export class PrpcClient { 48 readonly host: string; 49 readonly getAuthToken: () => string | Promise<string>; 50 readonly insecure: boolean; 51 readonly fetchImpl: typeof fetch; 52 53 constructor(options?: PrpcClientOptions) { 54 this.host = options?.host || self.location.host; 55 this.getAuthToken = options?.getAuthToken || (() => ''); 56 this.insecure = options?.insecure || false; 57 this.fetchImpl = options?.fetchImpl || self.fetch.bind(self); 58 } 59 60 /** 61 * Send an RPC request. 62 * @param {string} service Full service name, including package name. 63 * @param {string} method Service method name. 64 * @param {unknown} data The protobuf message object to send. 65 * @throws {ProtocolError} when an error happens at the pRPC protocol 66 * (HTTP) level. 67 * @throws {GrpcError} when the response returns a non-OK gRPC status. 68 * @return {Promise<unknown>} a promise resolving the response message object. 69 */ 70 async request( 71 service: string, 72 method: string, 73 data: unknown, 74 ): Promise<unknown> { 75 const protocol = this.insecure ? 'http:' : 'https:'; 76 const url = `${protocol}//${this.host}/prpc/${service}/${method}`; 77 78 const token = await this.getAuthToken(); 79 const response = await this.fetchImpl(url, { 80 method: 'POST', 81 credentials: 'omit', 82 headers: { 83 'accept': 'application/json', 84 'content-type': 'application/json', 85 ...(token && { authorization: `Bearer ${token}` }), 86 }, 87 body: JSON.stringify(data), 88 }); 89 90 if (!response.headers.has('X-Prpc-Grpc-Code')) { 91 throw new ProtocolError( 92 response.status, 93 'Invalid response: no X-Prpc-Grpc-Code response header', 94 ); 95 } 96 97 const rpcCode = Number.parseInt( 98 response.headers.get('X-Prpc-Grpc-Code') || '', 99 10, 100 ); 101 if (Number.isNaN(rpcCode)) { 102 throw new ProtocolError( 103 response.status, 104 'Invalid X-Prpc-Grpc-Code response header', 105 ); 106 } 107 108 const text = await response.text(); 109 110 if (rpcCode !== RpcCode.OK) { 111 throw new GrpcError(rpcCode, text); 112 } 113 114 // Strips out the XSSI prefix. 115 return JSON.parse(text.slice(')]}\'\n'.length)); 116 } 117 }