Skip to content

gRPC-Web

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

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
A browser reaches gRPC through a translating proxy

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-web wrapper, 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 URL
import { 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);

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.

  • 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.
Why can a browser not call a native gRPC server directly?
What sits between a gRPC-Web browser client and native gRPC services?
Which streaming modes does gRPC-Web generally support in browsers?
What makes Connect attractive for new browser-facing APIs?