Client side streaming in gRPC-Web

In a previous post I introduced my open source project to bring GopherJS bindings to Improbable’s gRPC-Web client. I’m happy to say that the initial goal of supporting all features of the gRPC-Web client has been completed. I was initially going to leave it at that and wait for client side streaming to land in the WHATWG Streams API Standard, and subsequently added to the official grpc-web spec and probably the gRPC-Web client, but then I was sitting at the GolangUK conference and I had a brain wave. What if I could write a Websocket proxy, à la Travis Clines grpc-websocket-proxy but translate the Websocket messages to gRPC streaming messages?

I immediately started prototyping it with my grpcweb-example repo and a custom websocket proxy. I experimented with a few different ways of translating Websocket messages to HTTP2 framed messages according to the gRPC wire format spec, but in the end I reused the ClientTransport type from the gRPC transport package which made that part quite simple. I used the grpc package for inspiration here.

For reporting the status of RPCs I used the Websocket CloseMessage, which allows you to specify a message string and a code, which seemed too perfect to pass up on. I translated gRPC codes into the user defined implementation range of 4000+ in order to send it across the Websocket transport without upsetting standard compliant clients.

The full source for the Websocket proxy is of course on my github.

It was surprisingly easy to get everything up and running, and I’m pretty pleased with how it turned out. It’s not entirely done yet, as it doesn’t yet support fetch headers and trailers from the server. I added a client side streaming and a bidi streaming example to the grpcweb-example app to show off the capabilities. I’m particularly happy with how the client chat example turned out, both in the frontend and the logic for distributing the messages in the backend. Here’s an excerpt:

// Send join message before user joins
s.b.Broadcast(srv.Context(), name+" has joined the chat")

listener := make(chan string)
err = s.b.Add(name, listener)
if err != nil {
    return err
}
defer func() {
    s.b.Remove(name)
    s.b.Broadcast(context.Background(), name+" has left the chat")
}()

Check out the full source on my github.

Adding the proxy to your gRPC server looks a little like this:

gs := grpc.NewServer()
wrappedServer := grpcweb.WrapServer(gs)

var clientCreds credentials.TransportCredentials
if *host == "" {
    var err error
    clientCreds, err = credentials.NewClientTLSFromFile("./insecure/localhost.crt", "localhost:10000")
    if err != nil {
        logger.Fatalln("Failed to get local server client credentials:", err)
    }
} else {
    cp, err := x509.SystemCertPool()
    if err != nil {
        logger.Fatalln("Failed to get local system certpool:", err)
    }
    clientCreds = credentials.NewTLS(&tls.Config{RootCAs: cp})
}

wsproxy := wsproxy.WrapServer(
    http.HandlerFunc(wrappedServer.ServeHTTP),
    wsproxy.WithLogger(logger),
    wsproxy.WithTransportCredentials(clientCreds))

Cribbed straight from my grpcweb-example repo. This nicely shows how to work with both local certificates and with on signed by a trusted CA. Also note the use of the Improbable grpcweb.WrapServer in order to support gRPC-Web as well.

So to conclude, we now have support for client side and bidirectional streaming in the GopherJS gRPC-Web bindings, thanks to Websockets and our Websocket proxy.

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!