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  }