gRPC-Web
The browser can’t do it
Section titled “The browser can’t do it”Native gRPC needs precise control over HTTP/2 frames — trailers, flow control, and the raw framing we saw in the foundations module. The browser fetch API deliberately hides all of that. JavaScript in a page simply cannot open a raw HTTP/2 stream and write gRPC frames onto it.
So a browser cannot call a gRPC server directly. That is not a bug you can configure around; it is a boundary of what browser APIs expose.
gRPC-Web: a browser-friendly dialect
Section titled “gRPC-Web: a browser-friendly dialect”gRPC-Web is a variant of the protocol designed to fit inside what browsers can do. It carries the same protobuf messages, but frames them so they survive a normal HTTP request — including encoding trailers (where gRPC puts its status code) into the response body, since browsers can’t read HTTP/2 trailers.
The catch: a gRPC-Web request is not the same bytes as a native gRPC request. Something has to translate between them.
You need a proxy
Section titled “You need a proxy”That translator sits between the browser and your gRPC services:
flowchart LR browser["browser gRPC-Web client"] -->|gRPC-Web over HTTP/1.1 or /2| proxy["proxy (Envoy / in-process)"] proxy -->|native gRPC over HTTP/2| backend["gRPC backend"] backend --> proxy --> browser
Two common ways to run that proxy:
- Envoy with the gRPC-Web filter — a dedicated edge proxy that translates gRPC-Web to native gRPC. Common when you already run Envoy.
- In-process handlers — many stacks (Go’s
grpc-webwrapper, others) let the gRPC server also accept gRPC-Web directly, no separate proxy needed.
On the client, you use a generated gRPC-Web stub instead of the native one:
// Browser (gRPC-Web) — generated client talks to the proxy URLimport { UserServiceClient } from './gen/user.client';import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';
const transport = new GrpcWebFetchTransport({ baseUrl: 'https://api.example.com' });const client = new UserServiceClient(transport);
const { response } = await client.getUser({ id: 42 });console.log(response.name);// Server-side: let the same gRPC server also accept gRPC-WebgrpcServer := grpc.NewServer()userv1.RegisterUserServiceServer(grpcServer, &server{})
wrapped := grpcweb.WrapServer(grpcServer) // accepts gRPC-Web requestshttp.ListenAndServe(":8080", wrapped)# Python has no browser runtime; front Python gRPC services with# Envoy configured with the grpc_web filter to serve browsers.# envoy.yaml (excerpt):# http_filters:# - name: envoy.filters.http.grpc_web# - name: envoy.filters.http.routerConnect: the modern alternative
Section titled “Connect: the modern alternative”Connect (from Buf) is a newer take that speaks three protocols from one server: its own simple Connect protocol, gRPC, and gRPC-Web. Its Connect protocol works over plain HTTP/1.1 and can carry JSON, so a browser — or even curl — can call it with no proxy at all, while backend services still use gRPC. For new browser-facing work, Connect removes most of the gRPC-Web friction.
What it means for your design
Section titled “What it means for your design”- Plan the proxy from day one if a browser is a client. gRPC-Web is not optional glue you add later; it’s the only way in.
- Streaming is limited. gRPC-Web supports unary and server-streaming well; client-streaming and bidirectional streaming are not generally available in browsers.
- Consider Connect for greenfield browser APIs — one server, three protocols, no separate proxy.