gRPC-Web with GopherJS

In a previous blog series I’ve talked about how to work with a gRPC backend from the GopherJS world. It relies on the gRPC-gateway which is a great piece of tech, but unfortunately carries a couple of downsides:

  1. Clients don’t know what types are used - the interface is HTTP JSON. This can be somewhat mitigated with the use of swagger generated interfaces, but it’s still not perfect.
  2. The interface being JSON means marshalling and unmarshalling can become a significant part of the latency between the client and the server.
  3. The gRPC-gateway requires specific changes to the proto definitions - it’s not as straightforward as just defining your RPC methods.

Fortunately, with the release of a spec compliant gRPC-Web implementation from Improbable, we can finally start enjoying the benefits of a protobuf typed interface in the frontend. This deals with all the mentioned downsides of the gRPC-gateway;

  1. Interfaces are typed via protobuffers.
  2. Messages are serialized to binary.
  3. There’s no difference between exposing the gRPC server to the web client than any other client.

The Improbable gRPC-Web README also has a long list of benefits.

gRPC-Web in GopherJS

In the last couple of weeks I’ve been working on a GopherJS wrapper for gRPC-Web, and I’m pleased to say that it’s ready for others to play around with. The wrapper is comprised of the grpcweb GopherJS library and the protoc-gen-gopherjs protoc plugin. Together, they make it possible to generate a GopherJS client interface from your proto file definitions. The protoc-gen-gopherjs README contains a thorough guide into how to generate the client interfaces.

To give an idea of the usage, I’ve put together an example using the GopherJS React bindings created by Paul Jolly (@_myitcv). If you want to skip ahead, the source is available on my github.

I’m going to assume that if you’re reading this post you’re already familiar with how to implement the Go backend part of this, so we’ll jump right into the client. The only difference in the backend from a normal Go gRPC server is the use of the Improbable gRPC-Web proxy wrapper. This is necessary as a translation layer from the gRPC-Web requests to fully compliant gRPC requests. There also exists a general-purpose proxy server, which can be used with gRPC servers in other languages.

The Client

The interface is generated using protoc-gen-gopherjs. The source protofile can be found in the repo. With the generated file we get access to the gRPC-Web methods GetBook and QueryBooks.

First off we need to create a new client:

client := library.NewBookServiceClient(baseURI)

The parameter is the address of the gRPC server, in this case the same address as we’re hosting the JS from, but it could be located on some external address. Note that gRPC-Web over HTTP2 requires TLS.

A simple request

Once we have a client, we can make calls on it just like on a normal Go gRPC client. The generated interfaces are designed to be as similar as possible to protoc-gen-go client interfaces. All RPC methods are blocking by default, though there are plans to expose an asynchronous API later on, if there is demand for it.

Lets get the book with an ISBN of 140008381:

req := &library.GetBookRequest{
    Isbn: 140008381,
}
book, err := client.GetBook(context.Background(), req)
if err != nil {
    panic(status.FromErr(err))
}
println(book)

The context parameter can be used to control timeout, deadline and cancellation of requests. The second parameter is the request to the method. Looks just like the normal Go client API.

Server side streaming

It wouldn’t be gRPC without streaming. Unfortunately, gRPC-Web does not currently support client-side streaming. We do have access to server side streaming though. This is a simple example of how to consume message from a streaming server side method:

req := &library.QueryBooksRequest{
    AuthorPrefix: "George",
}
srv, err := client.QueryBooks(context.Background(), req)
if err != nil {
    panic(status.FromErr(err))
}

for {
    // Blocks until new book is received
    bk, err := srv.Recv()
    if err != nil {
        if err == io.EOF {
            // Success! End of stream.
            return
        }
        panic(status.FromErr(err))
    }
    println(bk)
}

Much like the Go client API, we get a streaming server which we call Recv on until we see an error. If the error is io.EOF, it means the server has closed the stream successfully.

Wrapping up

With the release of an unofficial gRPC-Web client by Improbable, the frontend can finally start getting some of the benefits the backend has enjoyed for a couple of years now, courtesy of gRPC and Protobuffers. I’m personally extremely excited by the opportunities it affords frontend developers working with a simple frontend layer talking to a backend service. Take a look at the github repo for a complete example.

If you enjoyed this blog post, have any questions or input, don’t hesitate to contact me on @johanbrandhorst or under jbrandhorst on the Gophers Slack. I’d love to hear your thoughts!