Why gRPC needs a second protocol
gRPC is defined over HTTP/2 and depends on two things browsers do not give JavaScript: control over HTTP/2 framing, and access to HTTP trailers, where gRPC puts the status of every call. A browser cannot speak native gRPC, and neither can a lot of infrastructure that only handles HTTP/1.1 cleanly.
Two protocols answer that problem, and this page covers both because you choose between them, not in addition to them. gRPC-Web is the gRPC project’s own answer: a minimal change to the gRPC wire format that moves trailers into the body, so it works over any HTTP version, usually with a proxy translating to native gRPC on the server side. Connect is a newer protocol from Buf, now a Cloud Native Computing Foundation sandbox project, that goes further: unary calls are ordinary HTTP requests with ordinary JSON or binary Protocol Buffers bodies, readable in a network inspector and callable with curl.
The recommendation: if you are starting fresh and control the server, use a Connect implementation. Connect servers also accept gRPC and gRPC-Web requests natively, without a proxy, so you lose nothing. Use gRPC-Web on its own when you already run native gRPC servers and an Envoy layer, and only need to add browsers. For choosing RPC over REST at all, see choosing an API style.
1. Connect unary calls
A unary Connect call is a POST to /package.Service/Method with the message as the body:
POST /acme.weather.v1.WeatherService/GetCity HTTP/1.1Host: api.example.comContent-Type: application/jsonConnect-Protocol-Version: 1Connect-Timeout-Ms: 5000
{"cityId": "lon"}HTTP/1.1 200 OKContent-Type: application/json
{"name": "London"}| Header | Meaning |
|---|---|
Content-Type |
application/json or application/proto for unary calls |
Connect-Protocol-Version |
1. Clients should send it; servers and proxies may reject requests without it with 400 |
Connect-Timeout-Ms |
Deadline in milliseconds, a positive integer of at most 10 digits. Absent means no timeout |
The path is case-sensitive and may carry a routing prefix. Nothing else about it is designed: there are no path parameters and no query strings for unary POST.
GET for side-effect-free methods
A method marked idempotency_level = NO_SIDE_EFFECTS in its Protobuf definition can also be called with GET, which makes it cacheable by browsers and CDNs. Handlers support it automatically; clients have to opt in. The message moves into the query string:
GET /acme.weather.v1.WeatherService/GetCity?connect=v1&encoding=json&message=%7B%22cityId%22%3A%22lon%22%7D HTTP/1.1Host: api.example.com| Parameter | Meaning |
|---|---|
message |
The percent-encoded message |
encoding |
json or proto |
base64 |
1 when message is URL-safe base64, required for binary encodings |
compression |
A content coding such as gzip |
connect |
v1. Clients should send it; servers may reject GETs without it |
The timeout stays in the Connect-Timeout-Ms header. Enable GET for read methods that are hot and cacheable; it is the one thing Connect offers that native gRPC structurally cannot.
2. Connect errors
A failed unary call returns a non-200 status and a JSON body, always application/json even when the request used Protobuf:
HTTP/1.1 404 Not FoundContent-Type: application/json
{"code": "not_found", "message": "no city with id lon"}code is required; message and details are optional. Each entry in details has a fully qualified Protobuf type and a base64 value. The codes are gRPC’s, in snake case, with a fixed HTTP mapping:
| Code | HTTP | Code | HTTP |
|---|---|---|---|
canceled |
499 | aborted |
409 |
unknown |
500 |
out_of_range |
400 |
invalid_argument |
400 |
unimplemented |
501 |
deadline_exceeded |
504 |
internal |
500 |
not_found |
404 |
unavailable |
503 |
already_exists |
409 |
data_loss |
500 |
permission_denied |
403 |
unauthenticated |
401 |
resource_exhausted |
429 |
failed_precondition |
400 |
499 is the non-standard “client closed request” code; do not expect it from intermediaries. When a response has no Connect error body, for example a 502 from a load balancer, clients infer a code from the status: 401 becomes unauthenticated, 404 becomes unimplemented, 429 and 502 to 504 become unavailable, and everything else unknown.
Do not invent error codes or put the error in a 200 body. The fixed mapping is what lets generic HTTP tooling, retries and dashboards understand a Connect API without knowing it is one.
3. Connect streaming
Streaming calls use application/connect+json or application/connect+proto, always return 200, and frame every message in an envelope:
+--------+--------------------+------------------+| flags | length | message || 1 byte | 4 bytes, big-endian| length bytes |+--------+--------------------+------------------+Bit 0 of flags marks a compressed message; bit 1 marks the end-of-stream message, a final JSON object carrying error (absent on success) and metadata, which stands in for trailers. Because the status is in the body, a streaming response that fails halfway still has a 200 status line. Clients must read to the end-of-stream message before treating a stream as successful.
Servers should answer a streaming request whose content type does not begin with application/connect+ with 415 Unsupported Media Type.
4. gRPC-Web
gRPC-Web keeps the gRPC message framing (one flag byte, a four-byte length, the message) and changes only what the browser cannot do:
| Aspect | gRPC-Web |
|---|---|
| Content type | application/grpc-web, application/grpc-web+proto, or application/grpc-web-text for base64 |
| Transport | Any HTTP version; no dependence on HTTP/2 framing |
| Trailers | Sent as the last framed message, with the most significant bit of the flag byte set (0x80), encoded as an HTTP/1 header block with lowercase names |
| User agent | Clients send X-User-Agent, because the browser owns User-Agent |
POST /acme.weather.v1.WeatherService/GetCity HTTP/1.1Host: api.example.comContent-Type: application/grpc-web+protoAccept: application/grpc-web+protoX-User-Agent: grpc-web-javascript/0.1The -text variant base64-encodes the whole stream so that it survives environments that cannot handle binary response streaming.
The protocol is designed to be translated by a proxy. Native gRPC servers do not speak it, so something has to sit in front: Envoy is the default and supports it out of the box. The official grpc-web JavaScript client supports unary calls and server streaming (server streaming only in grpcwebtext mode); client streaming and bidirectional streaming are not supported.
5. Choosing between them
| Question | Connect | gRPC-Web |
|---|---|---|
| Callable with curl and plain JSON | Yes | No: binary framing even with JSON |
| Needs a proxy in front of the server | No | Yes, unless the server implements it |
| Cacheable GET for reads | Yes, for NO_SIDE_EFFECTS methods |
No |
| Works with existing native gRPC servers | Connect servers serve all three protocols | Yes, via Envoy |
| Browser streaming | Server streaming only | Server streaming only |
No browser protocol gives you client or bidirectional streaming today, because browsers do not support streaming request bodies across the board. If you need full duplex in a browser, that is a WebSocket problem, not an RPC protocol choice.
Best Practices
- Send
Connect-Protocol-Version: 1andconnect=v1from every client. Servers are allowed to reject calls without them, and some proxies route on them. - Mark read methods
NO_SIDE_EFFECTSand enable GET where responses are cacheable. - Never treat a streaming
200as success until the end-of-stream message or trailers arrive. - Use the snake-case Connect codes and their fixed HTTP statuses, not ad hoc error shapes, so the same client handles Connect, gRPC and gRPC-Web failures alike.