gRPC-Web
ActivegRPC-Web is an adaptation of the gRPC protocol for browser-based clients. Native gRPC requires HTTP/2 trailers, which browsers cannot send or access via the Fetch API or XMLHttpRequest. gRPC-Web solves this by encoding gRPC trailers as a special trailing data frame with the MSB set (flag byte 0x80 for uncompressed, 0x81 for compressed). A proxy (Envoy grpc-web filter, an in-process gRPC-Web interceptor, or Connect) translates between gRPC-Web and native gRPC on the server side. The Connect protocol (2022, Buf) is a newer alternative that eliminates the proxy requirement.
In one line
gRPC-Web adapts gRPC for browsers, which cannot send HTTP/2 trailers. Content-Type: application/grpc-web+proto. Each message is a 5-byte length-prefixed frame: 1-byte flag (0x00 = data, 0x80 = trailer) + 4-byte big-endian length + payload. Trailers are encoded as the last message with flag 0x80 containing key: value text pairs. A proxy (Envoy, Connect) translates to native gRPC/HTTP2. Server streaming works; client streaming and bidirectional streaming require the Connect protocol or browser fetch streams API.
Quick Reference
| Field | Size | Description |
|---|---|---|
| Content-Type | application/grpc-web+proto | Binary Protobuf encoding. Also: application/grpc-web (equivalent), application/grpc-web-text (base64 for CORS environments that cannot pass binary). The +proto suffix is explicit about encoding; omitting it defaults to Protobuf as well. |
| Frame format | 5-byte header + payload | Each message is framed: 1 byte flag + 4 bytes big-endian payload length + payload bytes. Flag 0x00 = uncompressed data message. Flag 0x01 = compressed data message. Flag 0x80 = uncompressed trailer message. Flag 0x81 = compressed trailer message. This matches the gRPC/HTTP2 length-prefixed message format. |
| Trailer encoding | Flag byte 0x80 | gRPC trailers (grpc-status, grpc-message, grpc-status-details-bin) cannot be sent as HTTP/1.1 trailers. gRPC-Web encodes them as the final frame with the MSB set (0x80). The payload is a CRLF-separated sequence of header-like lines: grpc-status: 0\r\ngrpc-message: \r\n. The proxy unwraps this into real HTTP/2 trailers for the gRPC server. |
| Proxy requirement | Envoy / in-process / nginx | gRPC-Web requires a translation proxy between the browser and gRPC server. Options: (1) Envoy grpc_web HTTP filter – production standard, (2) grpc-web in-process proxy (Go/Node.js library wrapping the gRPC server), (3) nginx grpc_pass module with header rewriting, (4) Connect protocol – proxy-free alternative for unary calls. |
| Streaming support | Server streaming only | Standard gRPC-Web supports unary (one request, one response) and server streaming (one request, N response frames). Client streaming and bidirectional streaming are not supported in standard gRPC-Web because browsers cannot stream request bodies using standard Fetch. Connect protocol with the Fetch streaming API enables bidirectional streaming in browsers that support it. |
| application/grpc-web-text | Base64 encoding | An alternative content type that base64-encodes all frames. Required in some CORS-constrained environments or CDNs that cannot pass binary response bodies. The base64 overhead is approximately 33% larger payload. Decoded by the client before processing. Not recommended for production unless CORS constraints require it. |
| grpc-status codes | 0 = OK, 1–16 = errors | gRPC-Web uses the same 17 status codes as gRPC: 0 OK, 1 CANCELLED, 2 UNKNOWN, 3 INVALID_ARGUMENT, 4 DEADLINE_EXCEEDED, 5 NOT_FOUND, 6 ALREADY_EXISTS, 7 PERMISSION_DENIED, 8 RESOURCE_EXHAUSTED, 12 UNIMPLEMENTED, 13 INTERNAL, 14 UNAVAILABLE, 16 UNAUTHENTICATED. Delivered in the trailer frame. |
| CORS headers | Access-Control-* | Browsers enforce CORS on gRPC-Web requests. The gRPC-Web proxy or server must return Access-Control-Allow-Origin, Access-Control-Allow-Methods: POST, Access-Control-Allow-Headers: content-type,x-grpc-web,x-user-agent, and Access-Control-Expose-Headers: grpc-status,grpc-message. Preflight OPTIONS requests are sent before POST requests. |
Key Characteristics
Browser-native gRPC calls
gRPC-Web lets JavaScript and TypeScript front-ends call gRPC services directly using generated client stubs. Type safety from .proto definitions flows to the browser. The grpc-web npm package and protoc-gen-grpc-web plugin generate TypeScript clients from the same .proto files used by backend services.
Proxy translates the protocol gap
The fundamental issue: HTTP/1.1 has no trailer support, and browsers cannot read HTTP/2 trailers via Fetch. The proxy unwraps gRPC-Web frames, reconstitutes HTTP/2 requests with real trailers, and translates responses back. Envoy's grpc_web filter handles this transparently in less than 1 ms overhead per request.
Connect protocol: proxy-free
Connect (Buf, 2022) solves the proxy requirement for unary calls. For unary: standard HTTP POST with Protobuf or JSON body, HTTP status carries gRPC status, no special framing. For streaming: same envelope framing as gRPC-Web but trailers delivered as a JSON object in the final frame. Connect servers accept gRPC, gRPC-Web, and Connect clients without configuration.
No bidirectional streaming without fetch streams
Standard gRPC-Web is limited to unary and server streaming because Fetch streaming request bodies are not universally supported. Full-duplex bidirectional streaming requires the Fetch upload streaming API (Chrome 105+, Safari 17.4+, Firefox partial) plus a Connect server. For broad browser support, bidirectional streaming should be replaced with WebSocket or SSE where possible.
Message Format
# gRPC-Web request: unary call (Protobuf)
POST /helloworld.Greeter/SayHello HTTP/1.1
Host: api.example.com
Content-Type: application/grpc-web+proto
X-Grpc-Web: 1
Accept: application/grpc-web+proto
# Body: 5-byte length-prefix frame + Protobuf payload
# Frame header:
# Byte 0: 0x00 (flag: uncompressed data message)
# Bytes 1-4: 0x00 0x00 0x00 0x0A (big-endian payload length = 10)
# Followed by Protobuf-encoded HelloRequest:
# field 1 (name): "Naveen"
# Full binary (hex): 00 00 00 00 0A 0A 06 4E 61 76 65 65 6E
# gRPC-Web server streaming request:
POST /helloworld.Greeter/SayHelloStream HTTP/1.1
Host: api.example.com
Content-Type: application/grpc-web+proto
X-Grpc-Web: 1
# Same framing as unary – server will return multiple data frames
# followed by one trailer frame (0x80 flag)# gRPC-Web response: unary (success)
HTTP/1.1 200 OK
Content-Type: application/grpc-web+proto
Transfer-Encoding: chunked
# Response body contains TWO frames:
# Frame 1: data message (flag 0x00)
# 00 00 00 00 0E (flag=0x00, length=14)
# <14 bytes of Protobuf-encoded HelloReply: message="Hello Naveen">
# Frame 2: trailer message (flag 0x80)
# 80 00 00 00 0F (flag=0x80, length=15)
# grpc-status: 0
(the trailer key-value text, CRLF-delimited)
# ── Error response (trailer carries non-zero grpc-status): ──────────────
# Frame 1: trailer message (0x80) with error
# 80 00 00 00 2A (flag=0x80, length=42)
# grpc-status: 5
# grpc-message: method not found
# ── Connect protocol (proxy-free unary): ────────────────────────────────
POST /helloworld.Greeter/SayHello HTTP/1.1
Host: api.example.com
Content-Type: application/proto
Connect-Protocol-Version: 1
# Standard HTTP body (no framing for unary)
# Response:
HTTP/1.1 200 OK
Content-Type: application/proto
# HTTP 200 = gRPC OK. Non-OK returns HTTP 4xx/5xx
# with JSON error body: {"code":"not_found","message":"..."}