{"id":16691351,"url":"https://github.com/dphilipson/grpc-web-hello","last_synced_at":"2026-04-29T06:35:10.259Z","repository":{"id":54751139,"uuid":"522640992","full_name":"dphilipson/grpc-web-hello","owner":"dphilipson","description":"My adventures trying out gRPC-Web with a Rust backend","archived":false,"fork":false,"pushed_at":"2022-12-30T09:41:30.000Z","size":7808,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-02T16:44:08.355Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dphilipson.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-08-08T17:15:17.000Z","updated_at":"2022-08-08T17:32:39.000Z","dependencies_parsed_at":"2023-01-31T11:00:54.775Z","dependency_job_id":null,"html_url":"https://github.com/dphilipson/grpc-web-hello","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dphilipson/grpc-web-hello","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dphilipson%2Fgrpc-web-hello","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dphilipson%2Fgrpc-web-hello/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dphilipson%2Fgrpc-web-hello/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dphilipson%2Fgrpc-web-hello/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dphilipson","download_url":"https://codeload.github.com/dphilipson/grpc-web-hello/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dphilipson%2Fgrpc-web-hello/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32414420,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-29T06:29:02.080Z","status":"ssl_error","status_checked_at":"2026-04-29T06:29:00.631Z","response_time":110,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-12T16:08:01.692Z","updated_at":"2026-04-29T06:35:10.244Z","avatar_url":"https://github.com/dphilipson.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gRPC-Web Hello\n\nOut of masochistic curiosity, I decided to make an end-to-end gRPC-Web setup\nwith a backend server in Rust, deployable as a standalone Docker image. I was\ncurious to see how the development experience of gRPC with server-side streaming\ncompares to WebSockets, the latter of which has never been a favorite API of\nmine.\n\nMy goal was to build a simple toy program where when you open the page in your\nbrowser, it subscribes you to a stream of updates that tells you how many total\nsubscriptions there are and prints that number on the page. So if you were to\nopen the page in multiple tabs, the number should change in all tabs to be the\nnumber of currently open tabs, and it should go back down in all tabs as tabs\nare closed.\n\nI'm kind of bad at computers, so this took me a lot longer than I was hoping.\nHere are some of the things that went wrong.\n\n## Frontend code\n\n- At this moment, the latest version of the Protobuf compiler doesn't work for\n  generating JS code! It's been like this for a full month, which is kind of\n  mind-blowing. See the [related GitHub\n  issue](https://github.com/protocolbuffers/protobuf-javascript/issues/127).\n\n  - To solve this, I thought I'd put in some legwork and build the protobuf-js\n    plugin from source as described in [@johejo's\n    instructions](https://github.com/protocolbuffers/protobuf-javascript/issues/127#issuecomment-1204202870).\n    The build failed when I ran it on my Mac (and I went through all the trouble\n    of installing Bazel too). Next I tried doing the build in a Debian\n    container, which succeeded, but then the executable wouldn't work on my Mac\n    outside the container. Rather than putting more time into this, I just\n    downgraded protoc as described [by @clehene\n    here](https://github.com/protocolbuffers/protobuf-javascript/issues/127#issuecomment-1204202844).\n\n- Generated JS protobuf files greatly increase the frontend bundle size, adding\n  230 KB parsed and 46 KB gzipped. This is because the generated files depend on\n  `google-protobuf`, a gigantic dependency seemingly not subject to treeshaking\n  or dead-code elimination. This is very shitty, and at the moment it seems that\n  anyone using gRPC-Web just has to live with it.\n\n- When you want to use server-side streaming, you need to put your generated\n  gRPC stuff into \"text mode\", meaning it sends all its protobuf messages as\n  base-64 strings rather than binary data. This means that many messages are\n  actually larger than equivalent JSON. Hopefully the size increase is negated\n  by compression, but it still feels a little wasteful.\n\n## Vanilla gRPC with Rust\n\nMy next goal was to build a normal Rust gRPC server (not gRPC-Web) based on\n[Tonic](https://github.com/hyperium/tonic), then run it out of a container.\n\n- At first, this worked fine locally but failed in the container. As it turned\n  out, the issue was this line, copied from an example:\n\n  ```rust\n  let addr = \"[::1]:50051\".parse()?;\n  ```\n\n  For reasons I don't understand, to get this to work out of a Docker container\n  and not just locally, I needed to change it to\n\n  ```rust\n  let addr = \"[::]:50051\".parse()?;\n  ```\n\n- The only other tricky part of the Rust implementation was detecting when the\n  client has cancelled out of a server-side stream. The way Tonic indicates this\n  is that the `Stream` that our method returns is dropped, so we need to provide\n  a custom `Stream` implementation that has a custom implementation of the\n  `Drop` trait. This conceptually makes sense (it means the program will never\n  end up hanging on to a `Stream` instance after the client has left), but felt\n  a bit clunky to actually work with. I could probably make this better by\n  writing some helpers.\n\n- I'm glad I did this custom `Stream` implementation because it requires this\n  method to be implemented:\n\n  ```rust\n  fn poll_next(self: Pin\u003c\u0026mut Self\u003e, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cOption\u003cSelf::Item\u003e\u003e\n  ```\n\n  which forced me actually learn what the `Pin` trait means. I've tried to learn\n  this before but [the docs](https://doc.rust-lang.org/std/pin/index.html)\n  always scared me off. For a while I thought I would have to implement what the\n  docs call [structural\n  pinning](https://doc.rust-lang.org/std/pin/index.html#pinning-is-structural-for-field)\n  which is a little worrying because it uses `unsafe` code, but as it turns out\n  I didn't need to get into that at all.\n\n- I spent a bit of time trying to get the build to work on Alpine just for the\n  challenge. I couldn't get it to work and I think it's not worth the trouble. I\n  can afford my images being a few tens of MB larger to not have to deal with\n  musl, plus it'll get much worse later anyways because of Envoy as we'll see in\n  the next section.\n\n## To gRPC-Web\n\nNow to turn gRPC into gRPC-Web. This means we need to run a reverse-proxy in\nfront of our gRPC server from the previous section. The proxy will do something\ninvolving turning the browser's HTTP/1.1 into the HTTP/2 our gRPC server\nunderstands.\n\nI wasted a **lot** of time here trying to do this with Envoy first, because\nthat's the only proxy mentioned in the docs on the [gRPC\nwebsite](https://grpc.io/docs/platforms/web/). Let's see how that went.\n\n### Envoy\n\n- Running this locally at first wasn't bad. I spun up an Envoy container and\n  gave it the example configuration from the gRPC-Web docs. The only thing I\n  needed change from the example was\n\n  ```yaml\n  socket_address:\n    address: 0.0.0.0\n    port_value: 9090\n  ```\n\n  to\n\n  ```yaml\n  socket_address:\n    address: host.docker.internal\n    port_value: 50051\n  ```\n\n  since we want the container to forward to the locally running Rust server on\n  my host machine (and at a different port than the example).\n\n- One extremely frustrating bug here: while the example named the config file\n  `envoy.yaml`, I was naming it `envoy.yml`. As it turns out, that doesn't work,\n  even when you manually specify the config file. Envoy doesn't recognize the\n  `.yml` extension and tries to parse it as JSON! Wtf?\n\n- Now to make a standalone Docker image. I figured I'd try to pack Envoy and the\n  server into the same image, because it would be easy to deploy and configure.\n  Probably experienced devops engineers would say to have them separate so they\n  could scale differently or something, but I feel like this would make\n  deployment significantly more complicated if I just want to deploy a single\n  server. Maybe Kubernetes makes this easy, I dunno.\n\n- At first, I wanted to build the server in a `rust` image, then copy the\n  executable into the `envoyproxy/envoy` image. When I tried this, the server\n  wouldn't run in the Envoy image because of not having the right version of\n  glibc. I figured I had the choice of either installing Rust into the Envoy\n  image so I could build a compatible executable, or installing Envoy into a a\n  different image that was capable of running the executable. I went with the\n  latter, because I feel like my Rust server, the point of the whole exercise,\n  shouldn't be beholden to environmental oddities on the Envoy image.\n\n- So, I decided to use a `debian/buster-slim` image, copy the Rust executable\n  into it, and install Envoy into it via the [somewhat lengthy set of\n  commands](https://www.envoyproxy.io/docs/envoy/latest/start/install#install-envoy-on-debian-gnu-linux)\n  from their Getting Started guide.\n\n- The `debian:buster-slim` base image is 69 MB. My Rust gRPC server executable\n  is only 7 MB (!!). But adding Envoy to the image adds 200 MB. This puts the\n  total image size at 276 MB, the vast majority of which is Envoy and its\n  dependencies. Hope all that space helps it do its job well!\n\n### gRPC Web Proxy\n\nEventually I noticed that the gRPC-Web GitHub readme also mentioned another\nproxy, [gRPC Web\nProxy](https://github.com/improbable-eng/grpc-web/tree/master/go/grpcwebproxy).\nI should have just used this in the beginning. Its big advantages:\n\n- Much simpler configuration.\n- It's 15 MB instead of 200 MB.\n- It has really nice gRPC-specific logging.\n- It has a standalone executable, so it's easy to add to containers.\n- The docs actually say it's intended to be used as a companion process in a\n  gRPC server container, so I don't feel guilty about putting the proxy and\n  server in the same container anymore.\n\nI replaced Envoy and got this working in a tenth of the time.\n\n### tonic-web\n\nAs it turns out, I didn't need to do any of this because Rust's gRPC library\nsupports gRPC-Web on its own, if you use the `tonic_web` crate. There's an\nundocumented need to set up CORS when you do this, which I found buried in a\n[GitHub\nissue](https://github.com/hyperium/tonic/issues/1174#issuecomment-1332341548),\nbut once you do that this just works and we no longer need to run other\nprocesses.\n\nI'm still glad I did it the other way first, because you would need to do this\nif your server were in a language which doesn't have a gRPC-Web library.\n\n## Conclusion\n\nWas it worth it? The alternative to gRPC-Web would be to implement the\nsubscriptions with WebSockets instead. What are the pros and cons?\n\n### gRPC-Web advantages\n\n- Generated stubs. Kind of significant because implementing a WebSocket\n  interface from scratch is hard work, both on the client- and server-sides.\n- If the server has clients on both the frontend and backend, then all clients\n  can use the same API rather than having a separate APIs for frontend and\n  backend.\n\n### gRPC-Web disadvantages\n\n- Huge frontend bundle size increase (230 KB parsed, 46 KB gzipped).\n- Can't reasonably use the Network tab to debug payloads.\n- Unclear if it provides any benefits in speed or bandwidth over JSON, due to\n  using base-64 text rather than binary.\n- As of today (August 8), the latest version of protoc is completely broken for\n  JavaScript, which lowers my faith in how well this is supported.\n\nThe first drawback is the worst one and it's terrible. And yet, in spite of it\nI'm still tempted to keep using gRPC-Web purely to avoid writing WebSocket\ncommunications by hand, with all of their error-checking and state management in\nboth the client and server.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdphilipson%2Fgrpc-web-hello","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdphilipson%2Fgrpc-web-hello","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdphilipson%2Fgrpc-web-hello/lists"}