The Connect protocol
Why special-case unary RPCs?
The Connect protocol is effectively two protocols, one for unary RPC and one for streaming. This isn't intellectually satisfying — it would be purer to treat unary RPCs as a bidirectional stream with exactly one message sent in each direction. In practice, we've found the loss of purity well worth it. By using standard HTTP compression negotiation and eliminating binary framing from the body, the Connect protocol lets us make unary RPCs with web browsers, cURL, or any other HTTP client.
Why not use HTTP status codes?
Every taxonomy of errors is flawed, but at least HTTP status codes are time-tested and widely understood. In a perfect world, we'd have used HTTP status codes as-is for the Connect protocol. Unfortunately, we want Connect handlers and clients to support the gRPC wire protocols without code changes. Since the mapping between gRPC and HTTP status codes is lossy, we can't provide an acceptable gRPC experience without adopting the same set of codes. C'est la vie.
Why not use the Twirp protocol?
We really like Twirp's protocol! It's simple, doesn't rely on any HTTP/2-specific framing, and works nicely with general-purpose HTTP tools. Unfortunately, it didn't fit our needs:
- It doesn't support streaming RPCs. Even if most RPCs are unary, many organizations have a handful of APIs that do benefit from streaming.
- It's semantically incompatible with gRPC. Because Twirp doesn't specify how to encode timeouts and uses a very different error model, swapping protocols requires significant code changes.
In the end, we prioritized gRPC and gRPC-Web compatibility over Twirp support. We hope that Connect's unary protocol captures most of Twirp's magic while still allowing your code to interoperate with the larger gRPC ecosystem.
Serialization & compression
Why are numbers serialized as strings in JSON?
Number is an IEEE 754 double-precision float: even though it
occupies 64 bits of memory, some of the space is reserved for the fractional
portion of the number. There's just not enough space left to represent 64-bit
integers! To make absolutely sure that integers are handled correctly, the
Protobuf JSON mapping represents the
uint64 types as
This only affects calls made with cURL, the browser's
fetch API, or other
plain HTTP tools. Connect clients automatically convert numeric values to and
Why use generics?
Generic code is inherently more complex than non-generic code. Still, introducing
connect-go eliminated two significant sources of complexity:
- Generics let us generate less code, especially for streaming RPCs — if
you're willing to write out some long URLs, it's now just as easy to use
protoc-gen-connect-go. The generic stream types, like
BidirectionalStream, are much clearer than the equivalent code generation templates.
- We don't need to attach any values to the context, because Connect's generic
Responsestructs can carry headers and trailers explicitly. This makes data flow obvious and avoids any confusion about inbound and outbound metadata.
On balance, we find
connect-go simpler with generics.
Why generate Connect-specific packages?
If you're familiar with Protobuf, you may have noticed that
protoc-gen-connect-go behaves a little differently from many other plugins:
rather than adding code alongside the basic message types, it creates a
separate, Connect-specific Go package and imports the base types.
This serves a few purposes:
- It keeps the base types lightweight, so every package that works with Protobuf messages doesn't drag along an RPC framework.
- It avoids name collisions. Many Protobuf plugins — including
protoc-gen-go-grpc— generate code alongside the base types, so the package namespace becomes very crowded.
- It keeps the contents of the base types package constant. This isn't critical when generating code locally, but it's critical to making remote generation work.
Why do I need a proxy to call gRPC backends?
The standard gRPC protocol uses HTTP trailers extensively. Apart from gRPC, trailers are rarely used and many HTTP implementations — including web browsers — don't support them. Without HTTP trailers, it's impossible for any code running in a web browser to support the standard gRPC protocol. The gRPC-Web protocol works around this gap by encoding trailers at the end of the response body, but most gRPC servers rely on Envoy to translate between the two protocols.
Is streaming supported?
While the Connect protocol supports all types of streaming RPCs, Connect-Web only implements server-streaming, because it is the only type of streaming that web browsers support across the board. The fetch API does specify streaming request bodies, but unfortunately, browser vendors have not come to an agreement to support streams from the client – see this WHATWG issue on GitHub.
Does generated code affect bundle size?
Yes, generated code does affect bundle size, but Connect-Web is a slim library with only around 1600 lines of code, and deliberately generates very little code. For an ELIZA client, the compressed bundle size is just under 11KiB.
How does Connect-Web compare to gRPC-web?
With Connect, you don't need a proxy to provide your gRPC service as gRPC-web, and TypeScript is supported out of the box. Requests are easy to inspect in the browser, because the JSON format is used by default, where gRPC-web only supports the binary format.
That said, Connect-Web ships with support for the gRPC-web protocol and is fully compatible with existing gRPC-web backends. See Choosing a protocol.