Skip to main content

Connect and gRPC-Web

A reference for the two protocols that carry Protocol Buffers RPCs to browsers and over HTTP/1.1: the Connect protocol, with plain JSON unary calls, GET for side-effect-free methods and JSON errors, and gRPC-Web, with trailers in the body and a translating proxy.

Current versionConnect protocol v1; gRPC-Web (PROTOCOL-WEB)
Specificationconnectrpc.com
Updated2026-10-17

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.1
Host: api.example.com
Content-Type: application/json
Connect-Protocol-Version: 1
Connect-Timeout-Ms: 5000
{"cityId": "lon"}
HTTP/1.1 200 OK
Content-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.1
Host: 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 Found
Content-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.1
Host: api.example.com
Content-Type: application/grpc-web+proto
Accept: application/grpc-web+proto
X-User-Agent: grpc-web-javascript/0.1

The -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: 1 and connect=v1 from every client. Servers are allowed to reject calls without them, and some proxies route on them.
  • Mark read methods NO_SIDE_EFFECTS and enable GET where responses are cacheable.
  • Never treat a streaming 200 as 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.
On this page (7 sections)