typescriptvue.jsenvoyproxygrpc-web

How to handle a gRPC-web unary call backend behind Envoy in Vue.js with protobuf-ts?


I am trying to implement a gRPC-web call in a Vue.js application that is similar to this example (https://github.com/timostamm/protobuf-ts/blob/master/packages/example-angular-app/src/app/grpcweb-unary/grpcweb-unary.component.ts). My proto file is as follows:

syntax = "proto3";
package extractor;

service Extract {
    rpc PutEntries (EntriesRequest) returns (EntriesResponse);
}

message EntriesRequest {
    repeated string value = 1;
}

message EntriesResponse {
    repeated string value = 1;
}

I have a gRPC server running on port 50051 with Envoy 1 on port 8080. It works when I use an external client (Kreya/BloomRPC).

The problem is that when I execute the call on the browser, I get a Uncaught (in promise) RpcError: upstream connect error or disconnect/reset before headers. reset reason: remote reset. CORS is enabled on Envoy and everything is running in the same machine (frontend, backend and envoy).

My Vue 3 component using TS:

<script setup lang="ts">
import { ref } from "vue";
import { ExtractClient } from "./grpc/extractor.client";
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
import type { EntriesRequest, EntriesResponse } from "./grpc/extractor";
import type { GrpcWebOptions } from '@protobuf-ts/grpcweb-transport';

const HOST = "http://localhost:8080";
const TIMEOUT = Date.now() + 2000;

let extracted = ref(false);
const fileInput = ref<HTMLInputElement | null>(null);
let filename = ref("");
let data = ref("");

let options: GrpcWebOptions = {
  baseUrl: HOST,
  timeout: TIMEOUT,
  format: 'binary',
  meta: {}
};

const handleFileChange = (event: Event) => {
      const input = event.target as HTMLInputElement;
      fileInput.value = input;
    };

function extract() {
  if (!fileInput.value) {
    return;
  }
  const file = fileInput.value.files![0];
  filename.value = file.name;
  const reader = new FileReader();
  reader.readAsText(file);
  reader.onload = async () => {
    const transport = new GrpcWebFetchTransport(options);
    const client = new ExtractClient(transport);

    const content = reader.result as string;
    const entries = content.split("\n").filter((entry) => entry !== "");

    // Convert the entries to the EntriesRequest format
    let request: EntriesRequest = {
      value: entries
    }
    
    // Make the grpc-web call to the PutEntries endpoint
    let call = client.putEntries(request, options);
    let response: EntriesResponse = await call.response;
    data.value = response.value.join("\n");
    extracted.value = true;
  };
}
</script>

<template>
  <form>
    <input type="file" @change="handleFileChange" />
  </form>
  <button @click="extract">Extract</button>
  <br /><br />
  <span v-show="extracted">Should show data from {{ filename }}
    <br />{{ data }}
  </span>
</template>

The restriction I have is not change the backend that is working with gRPC and use Envoy proxy. Can anyone provide idea on what may I be doing wrong?

Note: I am using protobuf-ts with "@protobuf-ts/grpcweb-transport", since I tried grpc-web and have problems with https://github.com/grpc/grpc-web/issues/1242.

UPDATE after @Brits answer: The application was setting I timeout when page starts, but even when I set it right before the call I get 503 Uncaught (in promise) RpcError: Service Unavailable. I am using a less than 1kb text file, and on Kreya client it responds in 2.1 seconds.

New extract method:

function extract() {
  if (!fileInput.value) {
    return;
  }
  const file = fileInput.value.files![0];
  filename.value = file.name;
  const reader = new FileReader();
  reader.readAsText(file);
    reader.onload = async () => {

    const content = reader.result as string;
    const entries = content.split("\n").filter((entry) => entry !== "");
    // Convert the entries to the EntriesRequest format
    let request: EntriesRequest = {
      value: entries
    }

    // Configure Grpc-Web client
    let options: GrpcWebOptions = {
      baseUrl: HOST,
      timeout: Date.now() + 10000,
      format: 'binary',
      meta: {}
    };
    const transport = new GrpcWebFetchTransport(options);
    const client = new ExtractClient(transport);
    
    // Make the grpc-web call to the PutEntries endpoint
    let call = client.putEntries(request, options);
    let response: EntriesResponse = await call.response;
    data.value = response.value.join("\n");
    extracted.value = true;
  };
}

Solution

  • As per the comments you are setting timeout when the page is loaded:

    const TIMEOUT = Date.now() + 2000;
    ...
    
    let options: GrpcWebOptions = {
      baseUrl: HOST,
      timeout: TIMEOUT,
      format: 'binary',
      meta: {}
    };
    

    This will set a deadline at 2 seconds after the page is loaded. However there is an issue here; as the docs say:

    Timeout for the call in milliseconds.
    If a Date object is given, it is used as a deadline.

    Date.now() + 2000; will return a number (a big number e.g. 1676577320644). If you want a Deadline use something like new Date(Date.now() + 2000);.

    As it is you are passing a very large number in as a timeout; my guess would be that this is not being handled correctly.

    As you note:

    the documentation states when dates are passed it is used as deadline:

    So an alternative is to just specify the number of milliseconds (e.g. 2000); this should result in the call being aborted after that number of milliseconds (which is what I believe you are intending).