1

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;
  };
}
staticdev
  • 2,950
  • 8
  • 42
  • 66
  • Please show the typescript code you are using to connect (something is wrong there because `http://localhost:5173/0.0.0.0:50051/extractor.Extract/GetEntries` should be `http://localhost:50051/extractor.Extract/GetEntries`). I'd expect your code to contain something like `let transport = new GrpcWebFetchTransport({baseUrl: "localhost:50051"});`. – Brits Jan 22 '23 at 02:16
  • @Brits connect code added. – staticdev Jan 23 '23 at 09:24
  • Please check for furthe messages in the browsers dev tools console. Also, have you verified that your server is working (using something like [evans](https://github.com/ktr071/evans) or [kreya](https://github.com/riok/Kreya)) – Brits Jan 23 '23 at 19:02
  • @Brits it works, I created a Golang client and can see the streaming working good. – staticdev Jan 23 '23 at 20:47
  • Is the golang client using grpc or grpcweb (I have not seen a grpcweb client library for go). – Brits Jan 23 '23 at 21:18
  • @Brits the client is using gRPC also the server. – staticdev Jan 24 '23 at 06:25
  • OK - you cannot use standard gRPC in the browser - you need to use gRPC web (with a Go app this is fairly simple to implement). See [this answer](https://stackoverflow.com/a/67308215/11810946) for details. – Brits Jan 24 '23 at 08:03
  • @Brits I set-up an Envoy proxy with the example configuration on grpc-web repo with CORS configured, pointed the application to the proxy and also same error "RpcError: NetworkError when attempting to fetch resource.". – staticdev Jan 27 '23 at 20:42
  • adding envoy into the mix significantly changes your question. I'd suggest creating a new one with full details (i.e. envoy config, port gRPC is running on, logs from envoy, details of how the web client is attempting to connect and any info from browser dev tools console). The fact that the app is written in Go become pretty much irrelevant (you have confirmed that gRPC clients can use the API). – Brits Jan 29 '23 at 21:29
  • @Brits I have made a revamp of the question then removing unnecessary golang details. Also putting a bounty on this one. – staticdev Feb 15 '23 at 21:36
  • Please try removing `timeout` (as you are setting this when the page is loaded the 2 seconds may well have passed by the time the button is pressed. Other than that I'd suggest taking a look at the [envoy logs](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/accesslog/v3/accesslog.proto#envoy-v3-api-msg-config-accesslog-v3-accesslog) to see if they provide any hints. – Brits Feb 15 '23 at 22:40
  • @Brits removing `timeout` worked! The envoy logs shows all the calls are being received but it answers as 503 for any timeout I set. I tried multiple values of timeout but the response is immediate. Maybe can you create a response explaining why timeout is the cause? – staticdev Feb 16 '23 at 06:29
  • UPDATE: After that I discovered if I replace `timeout` with `deadline` it works as expected. May be an issue since the documentation states when dates are passed it is used as deadline: https://github.com/timostamm/protobuf-ts/blob/master/MANUAL.md#rpc-options – staticdev Feb 16 '23 at 06:37

1 Answers1

1

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).

Brits
  • 14,829
  • 2
  • 18
  • 31
  • Thanks for your finding @Brits, but even when I change that is does not work. Only if I remove timeout completely. I have updated my question. – staticdev Feb 16 '23 at 08:44
  • I've updated my comment; have not actually tested this (my apps generally use `timeout: `10000` and work fine but I use [improbable-eng/grpc-web](https://github.com/improbable-eng/grpc-web). Anyway we are getting nearer to a solution! – Brits Feb 16 '23 at 20:13
  • Thanks, `timeout: new Date(Date.now() + 2000)` and `timeout: 2000` works as expected. I will use the second since it is simpler to read and is the default semantics of the word `timeout`. – staticdev Feb 17 '23 at 07:35