Connect: A Better gRPC
buf.build
buf.build
Right now, we've got a Go RPC library that's ready for early adopters. In a month or two, we'll have a TypeScript library for web developers. A while after that, we'll probably tackle server-side TypeScript. We're not sure what will be highest-priority after that: could be Swift + Kotlin for mobile, could be Python, could be something else.
We think that `connect-go` is a better choice than Google's gRPC implementations because it's (1) a simpler implementation, (2) interoperates better with the Go HTTP ecosystem, and (3) supports a nicer protocol _in addition to_ the gRPC and gRPC-Web protocols. You get everything that's good about gRPC, very little (hopefully none!) of what's bad, and you get some extra goodies on top. The same is true of the upcoming `connect-web` compared to `grpc-web`, and so on.
Re: JSON, you're right - proto3 defines a Protobuf-to-JSON mapping that everyone uses. What's less obvious is that if you use JSON with the gRPC protocol, you don't actually end up with JSON HTTP payloads. Instead, you get `<5 bytes of binary framing>{"some": "json"}` - which isn't all that useful, because it's no longer JSON. Then consider that gRPC doesn't use meaningful HTTP status codes, even for request-response RPCs, and it requires using HTTP trailers, and it requires HTTP/2. All of a sudden, it's not much like the ubiquitous, un-fussy HTTP APIs that make REST so successful. The `connect-go` library offers you a solution for this by supporting the Connect _protocol_, which fixes these warts.
Hopefully that's a little clearer <3
You haven't really actually answered the question except for hand waving and most of it is opinions against things that exist for good reason, without justifications.
The Go code it generates from .proto files are the first generated bindings I've seen that use Go generics. I always dreaded needing to look in the stock generated code, but I took a peek at the sample Connect-generated code and it's quite readable. Generics do help; a big part of the cognitive overhead for me in reading the existing bindings is how many "synthetic" types it adds that don't correspond to a named type in the .proto IDL. They have a bidirectional RPC in their example, and its generated Go method has a return type of `*BidiStream[ConverseRequest, ConverseResponse]`. In the grpc-go generated code, that would spawn a named interface type like `ElizaService_ConverseClient`. There's less to wade through.
But for me that's a "nice to have", and the big win I see is the unbundling of gRPC. What I've wanted is just a standard way to do Protobuf-defined RPC over HTTP, preferably one that the developers of API's I use have also adopted. gRPC is that, but when I've deployed it, I've deployed it within a service mesh that provides service discovery, load balancing, circuit breaking, backoff and retry behavior, etc. But gRPC clients include all of that, too, and you can't opt out. The gRPC client will think it has one subchannel open to a single service endpoint, when really it's got a localhost connection to a sidecar proxy providing transparent load balancing. I would just hope things ended up OK.
Distributed systems are complex enough, and the fewer state machines I have to internalize, the better I can understand the rest. Connect removes some of the incidental complexity from something that's meant to be a ubiquitous server-to-server protocol. I'm looking forward to TypeScript and Rust bindings being released!
Mostly because grpc-go's implementation is about 5-10x faster than net/http.
Maybe it follows the spec completely and defensively, requiring many more checks and supporting typically unused behavior
I also presume there are some reduction in bugs simply by reducing the LoC in this project compared to grpc-go.
i belive this will be huge. biggest blocker for grpc adoption is the really poor js and grpc-web implementations.
No affiliation, just a fan
Connect is for the rest of us. I wonder what kind of useful general-purpose interceptors folks will come up with?
They use this to pad the release notes so they don’t have to comprehensively document the behavior changes.
I want to laugh, but I also want to cry because it's true.
If you want code gen, just edit OAS specs with Stoplight’s OpenAPI editor, and generate clients/servers with OpenAPI generator. If you really need event streaming between services, just use something like Google PubSub, webhooks, Kafka, whatever - external systems are better for this than direct server to server comms because they’re more reliable, far less worries about deployment, etc.
RPC seems simple at first but always balloons into excess complexity. Just stick with REST, and sprinkle in a PubSub system if absolutely necessary, it’ll stay simpler that way.
[0] FlatBuffers would probably work well too, though I haven't used it
The latter has code-generation for services and has various transport packages for twirp, grpc, and grpc-web.
`connect-web` will add the RPC layer, built on top of the browser's `fetch` API.
To us, the biggest difference is that Connect _also_ supports the gRPC and gRPC-Web protocols. That lets your code interop with a much, much larger ecosystem of tools and systems, many of which are better-supported than their Twirp equivalents.
We designed the Connect protocol and the Go APIs to make multi-protocol support seamless. Server-side code supports the Twirp-like protocol and the gRPC protocols simultaneously by default. Clients can switch protocol with one option, no other code changes required.
It was the #3 issue opened on twirp repository, but they never settled on a solution.
https://github.com/twitchtv/twirp/issues/3
One super underrated feature Connect offers for Go devs is access to the request/response headers. No more plumbing incoming/outgoing context :phew:
I'd hope that when the typescript version is available that it actually uses promises, Async generators and supports both client side and server side. And most importantly we can use any underlying stream socket (like UDP).
> Streaming RPCs may be half- or full-duplex. In server streaming RPCs, the client sends a single message and the server responds with a stream of messages. In client streaming RPCs, the client sends a stream of messages and the server responds with a single message. In bidirectional streaming RPCs, both the client and server send a stream of messages. Depending on the Connect implementation, IDL, and HTTP version in use, some or all of these streaming RPC types may be unavailable.
I am also curious to check out if it assails my concerns about gRPC streams in its own API. I recall having a lot of trouble with figuring out whether gRPC would handle streaming connections hanging gracefully, for example.
> I recall having a lot of trouble with figuring out whether gRPC would handle streaming connections hanging gracefully, for example.
I think I know what you mean, I've always had a hard time differentiating from protocol errors and application errors. Ultimately I gave up trying to be smart and just treated any interruption the same way: resume the stream from where it left off. Transactional behavior is impossible within just gRPC, as far as I can tell.
I'm bemused by gRPC's status code system. It seems just as difficult to use as HTTP status codes, more or less, while also being less widely understood. If I were designing for myself, there would only be four error codes: maybe retriable and definitely not retriable, with an extra bit for whether or not the application returned the code explicitly. Sadly, we had to adopt gRPC's codes to make multi-protocol support work well. (Plus, I don't think anyone else agrees with my hot take on errors!)
Browser fetch also doesn't support streaming request body (on current day browsers)
https://web.dev/fetch-upload-streaming/ "It doesn't look like your browser supports request streams."
https://bugs.chromium.org/p/chromium/issues/detail?id=688906
In general, gRPC documentation is very clear because you're either in or out: if you have end-to-end HTTP/2 and trailer support, you're in and everything works. If you don't have both, you're all the way out and _nothing_ works. Connect aims for something more like progressive enhancement, where implementations support as much of the feature set as they can. Even if that's just unary (request/response) RPCs, it's still useful.
No matter what protocol you're using, bidirectional streaming requires:
1. A schema language with the concept of bidi streaming RPCs. If you're using Thrift, you're out of luck.
2. An HTTP/2 connection. If you're using Python and `requests`, you're out of luck.
If you have both of those, you're capable of bidi streaming. (gRPC's HTTP/2 protocol _also_ requires support for HTTP trailers.)
Protobuf supports bidi streaming RPCs. `connect-go` supports HTTP/2 (and trailers, for the gRPC protocol). So if you're using Protobuf schemas and a `connect-go` client and a `connect-go` server, you have full bidi streaming support and you can choose between any of the three supported protocols. If you're using a `grpc-go` client and a `connect-go` server, or vice versa, you have full bidi streaming support but only using the gRPC HTTP/2 protocol. The wire details differ between the gRPC, gRPC-Web, and Connect protocols, but the semantics are the same and all the Go APIs behave the same.
Hopefully that clarifies things. (If not, let me know and I'll take another run at it!)
I hope you build something awesome for the community!
This was what I missed when I did JMS for a project in Java. The tooling felt confusing and I never found good tooling to see the messages passed through and poke and prod at them.
connect-go looks very similar to what we’ve built. If you ever release Ruby bindings, there’s a strong chance we could switch to it. Nice work!
Ok, though, that really is the point of the article. And, it’s more than fair. Really hoping for a Java version of this.
Edit: That's _mostly_ correct! You don't need Envoy to have JS/TS running in a browser call your handler. If I'm being really picky, though, in that case the browser and your handler would need to use either the gRPC-Web or Connect protocol, not the backend gRPC-over-HTTP/2 protocol. Any `connect-go` handler you write supports all three protocols by default, so the short answer is still "yes!"
In short: what's the deal with all these fences, and who the hell was "Chesterton"?
I evaluated gRPC but the web story is a disaster; you need an envoy proxy, and TypeScript support was half-baked. I went with Twirp instead. If Connect had been around, I would have gone with it over Twirp because it's compatible with gRPC and Twirp makes a few weird choices like JSON for error messages.
When you would use JSON over HTTP vs gRPC? I can think of a couple cases:
1. Your infrastructure doesn't support HTTP/2 with trailers.
2. You need to send/receive payloads that aren't well-defined. The nice thing about JSON vs protocol buffers is that JSON is self-describing and flexible. In most languages you have libraries which can deal with JSON as essentially a Map<String,Object>, with maybe a nice cursor API that allows you to pick/update specific fields without knowing anything else about the object structure. You can encode the JSON AST in protocol buffers but it will not be very nice I don't think.
3. You want your wire-format to be human readable for some reason. You can technically do this with gRPC as it is not strictly tied to protocol buffers as the wire format for messages, but in practice it is kind of a pain and language support for doing it can be spotty. At the very least you have to figure out how to do it as protocol buffers is very much the default.
4. The code-generated classes/structs from gRPC IDL are often quite bad to work with. If you've ever used the case classes generated by scalapb (Scala) or the structs generated by Prost (Rust) then you know what I mean. You end up having to create a parallel set of data structures for your protobuf message types to keep yourself sane. With JSON on the other hand, you can typically create classes/structs which are nice to work with internally but also directly serializable to JSON.
All that said, gRPC is really nice. If I am bootstrapping a project, gRPC is definitely the default and I need a good reason to use anything else.
A new protocol specification that fulfills the goals of working in browser environments (no trailers) and is more debuggable? Seems to be like it - but then it wouldn’t really be compatible to regular gRPC and gRPC-Web.
A library which implements the new protocol and the existing ones? Apparently from how far how understand the page - but probably wouldn’t fulfill the „no-bloat“ goal too much.
https://connect.build/docs/go/serialization-and-compression#... covers this, and coincidentally uses flatbuffers as the example :)
1. A `connect.Codec` implementation. I think it'd look almost the same as our Protobuf codec [0], but you'd use `FlatbuffersCodec.Marshal` and `FlatbuffersCodec.Unmarshal` (from github.com/google/flatbuffers/go). You _must_ have a `Codec`, but it looks like it should be pretty quick.
2. Ideally, you'd have a standalone program to parse your Flatbuffer schema and produce Connect code. The output would probably be very similar to `protoc-gen-connect-go`'s [1]. This isn't required, but it's a nice quality of life improvement (check out [1] to see the kind of conveniences it adds). With Protobuf, you'd do this via a `protoc` plugin and you wouldn't need to parse the schema yourself. From a quick look, I don't think `flatc` supports plugins at all - so maybe you'd either skip this step or put in the effort to parse the schema yourself?
[0]: https://github.com/bufbuild/connect-go/blob/main/codec.go#L5...
[1]: https://github.com/bufbuild/connect-go/blob/main/internal/ge...
Also, thank you for making me stop and think about this in detail. I just pulled Flatbuffers out of a hat when I was writing that part of the docs and assumed that `flatc` supported plugins like `protoc`. Turns out that when you assume... I'm updating that portion of the documentation now :)
How strongly connect relies on gRPC? Because it sounds like it’s easy to lock in to buf’s ecosystem.
Connect supports gRPC fully, so you shouldn't ever be locked in - you can always migrate systems to `grpc-go`, one piece at a time. We intend all the Connect projects to be community projects - we're happy to accept contributions, design proposals, and anything else. At the same time, we're committing our resources to keeping these projects well-maintained.