The state of gRPC in the browser
grpc.io
grpc.io
- Typing all the way to the frontend.
- gRPC/protobuf forces you to describe your interfaces and are self-documenting.
- gRPC semantics like error codes and deadlines (if you propagate them through your stack, they're particularly useful - for instance, we cancel database transactions across service boundaries if a request times out).
- Performance is great (but we're far from seeing bottlenecks with JSON, it's not the reason we choose gRPC).
- We use grpc-gateway which auto-generates a REST proxy for our customers. We sometimes use it for interactive debugging. [1]
- Rather than importing database models for our management tools and one-off scripts, using the API is so frictionless that we even use it inside our backend code and for CLI utilities.
The Google API design guide is helpful: https://cloud.google.com/apis/design
One piece of advice: Treat your gRPC calls like you would treat a GraphQL resolver - if you squint your eyes, they're very similar concepts.
Rather than specifying a GraphQL query, you specify a Field Mask.
https://developers.google.com/protocol-buffers/docs/referenc...
Happy to answer questions on our experience.
Questions:
1) If you have TS on the front and back end - you already have de-facto typing. Why did you opt for gRPC over just some kind of basic serialization, even JSON? I
2) If you have TS classes, then you need to maintain a set of Protobuff service definitions as well - do you find this was a lot of duplicated overhead?
3) gRPC seems to me to be a little 'internal', not quite a web tech meant to be shared between parties. Did you have trouble with authentication, especially with 3rd parties? Or other 'cross-cutting' concerns?
I'm literally looking at doing what you're doing, any feedback would be greatly appreciated.
2) The TS classes are auto-generated and the protobuf definitions are the only authoritative definition.
3) We're using JWT for authentication (in particular, short-lived tokens issues by Keycloak, using their JS library). gRPC has request metadata and it was straight-forward to use it with the Keycloak library.
We generate client libraries for customers to use and give them the option of using the REST API if they don't want to deal with gRPC. We consider the gRPC API to be a competitive advantage - our customers are enterprises and like well-defined and typed interfaces.
- Very complete OpenID Connect implementation.
- Back-channel logout.
- Short-lived tokens and lots of tooling to handle refreshing them. This solves many of the issues associated with JWT expiration.
- Documentation is very complete.
- Their data model allows for very fine-grained access control - you can issue sub-tokens with limited scopes, limit scopes per client and so on.
- Easy to deploy in OpenShift/k8s.
Only pain points are the build process (Java has got nothing on NodeJS as far as number of dependencies go!), data migrations and the fact that it's written in Java (start up time in CI).
One of the biggest selling points of gRPC (or many of the protobuf based systems, like Twirp) is that there's a huge community of people building code-gen tools for the API and Client out of a .proto file. What this means is that you can auto-generate a typescript library in one command, then publish that to npm.
There's an implicit guarantee that, if the TS client library and, say, the Go backend, are both "the same version" of the API (meaning, generated from the same .proto file), you'll get full API compatibility. And its not just "the frontend is assuming the response is of type T" or "doing schema validation on the response once you get it to make sure it conforms to a type the frontend defines independently"; the client lib and the backend lib are both generated from the same source of truth.
This exists with something like OpenAPI, but Protobuf is much simpler. And then you layer something like gRPC on top of that to get all the HTTP2 benefits, and its obvious why its gaining popularity.
That being said, to the third question: There's a huge advantage in those auto-generated client libraries. That's literally how libraries like the Google Cloud SDK are made; autogenerated from protobufs. But, if you use gRPC, the state of using that on the frontend natively sucks. So its got pros and cons.
Personally, we've opted to use Twirp instead of gRPC for the time being. Its all the advantages of protobufs, but it creates an API that is easily callable by web clients because its just a simple HTTP endpoint with a JSON payload. The only big thing you lose is the HTTP2 streaming, which might come one day.
...where it is transpiled to ES5... :(
Edit: I know I might get downvotes but think for a second we could have just taken some good parts from SOAP to begin with and have all the goodness.
gRPC and other modern RPC protocols/SOA frameworks are basically "the good parts from SOAP".
Industry standards tend to take on a life of their own, even if they came from one company. (Consider Docker.)
You might also be interested that Google is hosting the first ever gRPC Conf, which is being organized by CNCF the gRPC community: https://events.linuxfoundation.org/events/grpconf-2019/
(Disclosure: I'm executive director of CNCF.)
It's no better now :) I use it frequently with SOAP endpoints from various companies (all with varying authentication measures/encodings/versions) and it's a nightmare.
Unfortunately the travel industry hasn't gone for anything else yet...
SOAP has a broken type system that can not be extended (leading to many extension standards for things like lists (go figure, again) and complex formats that people didn't want to convert from and into). It had horrible error handling, with problems only becoming visible at the application layer. It inherited the XML's property vs. contents problem. It inherited the XML's bad DTD format.
Soap deserves to die. Taking the good parts of it would be good, but there was nobody in a position to do that, so we settled on fixing the problems and patching the good parts back with time. With gRPC, I think we are there.
Then, HTTP itself does the RPC message type management. Request message the server didn't expect? 406 error.
Hell no! CORBA was a binary protocol, but it was much easier to reason about and debug than SOAP.
Somebody put that "simple" there with no concern about semantics.
I was never able to write a complete SOAP response, and I don't think I was once able to predict one without running first (parsing after the fact is easier, that I can do).
But really, all this serialization crap is what programmers are legendary for: trading one form of busywork (reading bytes into object fields) for an even more obscure form of busy work (specifying a generator of the process).
We have since switched to GraphQL and haven't looked back.
Can I ask a question?
Seems GraphQL and gRPC are to some extent complementary: one is graph/data oriented, the other more functional/service oriented.
I know GQL has it's own ideas about transport, but would it make any sense at all to actually put GQP over gRPC? And by that I mean to have some gRPC services pass GQL requests as parameters? Or have I misunderstood entirely?
Protos are a serialization contract and should remain such. Too much proto-specific logic ended up bleeding into our web codebase (dealing with oneofs, enums, etc). GQL's IDL on the other hand ended up being a perfect middle-ground. It gave us a nice layer to deal with that serialization specific stuff, while letting the front-end work with better data models (interfaces, unions, string enums, etc.). GQL's IDL and TypeScript are a great match, since GQL types are ultimately just discriminated unions, which TS handles like a charm.
I find this fascinating, because it seems that a lot of the bleed should be the same (isn't 'oneof' roughly equivalent to 'union'?)... but it sounds like something is different in practice, and I'd really like to understand what the root cause of the difference is.
GQL simultaneously addresses those two specific problems: resolution of normalized data, and giving UI consumers the power to declaratively fetch their desired data shape. It also has first-class TypeScript support through Apollo and the open-source community built around that.
I think it’s important to stress the tooling support, because you are correct… oneofs and union types are conceptually the same thing. A lot of it comes down to ergonomics in how you consume those types. In code generation GQL unions represent themselves as actual TypeScript union types, which means I can write type guards or switch on their discriminant to narrow to their correct member, whereas proto oneofs use a string value to access the correctly populated property. Small things in the day-to-day, but in how it manifested itself in code, it definitely felt like an improvement.
GQL unions also give you the power to do some really cool projection declaratively in queries [2]. Once again because of the nice compatibility of TS’ type system and GQL, the types returned from those queries code generate into really nice structures to work with.
I’m getting a bit rambly and don’t feel like I adequately answered your question, but it’s a bit late and I wanted to give you some response off the top of my head. I don’t want to knock on grpc-web or anything. A good deal of it has to deal with code ownership and team communication structures, and GQL ultimately felt like a better seam for our UI team to interact with our services.
I probably should write a blog post, because I have a lot of disconnected thoughts and need to have a more coherent narrative here. I think some code examples would better illustrate what seems like non-problems from how I've described them here. I’ll follow up once I’ve let it settle in my head.
[1] https://samnewman.io/patterns/architectural/bff/ [2] https://graphql.org/learn/schema/#union-types
And yes please do continue to write more, will eagerly read :)
They are very similar paradigm-wise.
For pure protobuf communication on embedded systems nanopb works fine. If one doesn't need concurrent streams, HTTP/1.1 or coap plus nanopb encoded payloads work fairly well.
It almost becomes RESTful where I have a proto that has a request field, response field, and all the optional fields for both, when the server or app sees one of these I need all custom implementation to decide what to do with these ‘states’. It seems wrong and complex to make it all arbitrarily handled at each of three places.
NanoPB is excellent. But it’s the handling of the data (calls, req/res, timeouts, errors, formats, patterns) that I wish there was something for.
And yea, you got it right that with no HTTP layer it wouldn’t be gRPC on the device. But my app and server could still use that, if only the device could process the intention of the gRPC format changes / extensions.
But with the HTTP Gateway you are actually supporting both, so a browser client can still use GRPC-Web if it is able.
Retrofit seamlessly converts JSON data by means of json de-serializer, into Java classes. So we get typefull semantic (but, albeit at run time only).
I have retrofit integrated with RxJava, and when I looked what it would take to 'plugin' gRPC, I found that I have to change not just my backend, but all the wiring for the mobile frontends. And, at the time it looked like too much work.
If Retrofit, would make it transparent JSON vs gRPC -- then it would be great for folks that already invested into Retrofit/JSON.
- A specification language for services and the RPC calls they take (except RPC response primitives include single message and stream)
- A binary object definition/packing scheme (aka protobuf)
Outside of the HTTP and what HTTP/2 makes possible, I feel vaguely like everyone is rushing to replace pure HTTP/HTTP2 (and HTTP3 in the future) with something that is less extensible and could be implemented inside HTTP for the most part.
Obviously you can't do anything about the inefficiencies of headers in HTTP/1 (this is better in HTTP/2 & 3), the lack of built-in stream semantics (again only HTTP/1 though you can make do with some longpolling/SSE/websockets scheme) but outside of the HTTP stuff you can absolutely transmit your content as a stream of tightly packed bytes and let consumers do whatever they need to...
Why is gRPC anything more than a content type? It's so weird to see gRPC evolve from (in some sense, I might be wrong) HTTP + HTTP/2, and now people trying to shoe horn it back into the browser.
It's really hard to find metrics (maybe I should do a comparison and post results I guess), but like in this SO post[0]. The hype train is presenting gRPC as the next thing, but it shouldn't be, IMO. Most of the improvement is from use of protobuf for more efficient (de)serialization -- and I don't think gRPC is the best tool for declaring API schemas (RPC or otherwise) either.
[0]: https://stackoverflow.com/questions/44877606/is-grpchttp-2-f...
I think even streaming should have been possible with HTTP/1.1. Bodies can be streamed there just fine, if libraries support it (for browsers the issue was up to now that the APIs don't support access to bodies as streams). The only thing I'm not sure if there is an issue in HTTP/1.1 with request streams still running while the response stream has already finished.
This is also my understanding (minus the Trailers bit) -- there's stuff like grpc-gateway[0] that I've considered using before so I know there's a mapping (whether it's easy to use is another thing) from grpc to HTTP/1.1 ...
> I think even streaming should have been possible with HTTP/1.1. Bodies can be streamed there just fine, if libraries support it (for browsers the issue was up to now that the APIs don't support access to bodies as streams). The only thing I'm not sure if there is an issue in HTTP/1.1 with request streams still running while the response stream has already finished.
For streaming I was thinking mostly of the duplex streams, i.e. what Websockets brought to the table, everything else would be pretty hacky. I suspect that grpc translates to regular browser-ready HTTP/1.1 REST pretty easily (outside of trying to decide how), minus the streaming bit, since you'd have to do some sort of comet/long polling/sse/websockets approach.
Even just for Electron the built in IPC API doesn’t really cut it long term since you don’t get typing nor proper request/response or topic based streaming support so bugs are more likely to sneak in.
EDIT: Better question: is anyone aware of a system like gRPC but built with bidirectional streaming for the browser from the beginning?
HTTP/2 (i.e. gRPC) is a bidirectional streaming protocol, and you can use the fetch API in JS to use it. The reason gRPC-web exists is because browsers artificially hide some of the headers, which have been part of the HTTP standard since the beginning. If that was fixed, gRPC would just be plain XHR or fetch requests and gRPC-web would go away.
I don't explicitly mention it in the post, but no browser has support for fetch request streaming yet, so true bidirectionality would not be possible even if you had control over the headers. This will come eventually, and then grpc-web will have proper bi-di streaming support. It is doubtful whether it will actually have access to raw HTTP/2 frames which would be required for the gRPC HTTP/2 protocol.
Also, is head of line blocking really that big of a problem for RPC? It only seems to really be a big deal if your latency tolerance is really tight.
And a server that implements it:
http://crossbar.io https://github.com/crossbario/autobahn-js
Meteor was seriously ahead of its time, much like all good systems that suffered under their complexity before the infrastructure was there to support it. At its core, its a websocket-based RPC protocol that could return database cursors, with asynchronous downloading of database documents. The client ran a miniature instance of MongoDB inside the browser, which enabled clients to issue normal MongoDB queries client-side with optimistic evaluation. Data updates would hit the local mongo instance, be optimistically rendered, and then be sent to the server. Then the source-of-truth would be delivered back to the client via the mongodb oplog.
In other words, it's bi-directional mongodb oplog streaming. Absolutely fantastic considering it was released in 2012.
The advantage of protobufs is basically static typing with backward-compatible serialization (you can add new fields), that's compatible with many server-side languages.
It's honestly an awkward fit for web apps, though. Basic things are different, like int64's aren't native to JavaScript.
gRPC gives you guarantees on those inputs and outputs, along with a bunch of extra features. It's basically a layer on top of XHR.
how is this still "the future"?
Being limited to types that Java supports is a huge limiting factor for some use cases, thankfully there are extensions to the spec to get around these limitations (but then you have to use libraries that all supports the same extensions, and that limits your choices, so yes this can become an issue!)
On the flip side, compared to JSON with its anemic type support, Protobufs looks great!
Of course at the end of the day, a huge % of apps have to talk to the web browser, so everything gets dumbed down to strings and doubles. :(
Am I the only one who wants to keep serialization as far from my type system as possible? I want to have my internal data model for my software, and when I want to serialize it, I should be able to do it in any number of different ways (redacting values, using HATEOAS links for REST routes, using a lossless format for storage) depending on the situation.
Protobuf couples data modeling and data serialization into a single inseparable concern, and it becomes difficult to do anything else once you start using it.
Sure you can use JSON and make everything strings and do something like
{ type: 'int8', value: '52' }
but then parsing out the data ends up being a huge overhead.I once saw an XML variant of this, where someone decided to make the ultimate Distributed Computing System and serialize every single function call, take up over 1/2 of a CPU core for de/serialization for just the one app running on that machine.
While I could admire the purity of the design, it was utterly insane as an actual thing to bring into the world.
Protobuf is a good mixture of "strict typing so everyone is on the same page", "freedom to do stuff", and "perf isn't horrible."
There are other encoding systems out there that offer even more freedom, e.g. cap'n'proto, and better performance, but with other trade offs.
On the opposite side of things, my team had been serializing straight C structs out over the wire, but every field addition was a breaking change, and communicating the changes to our structures across teams was a nightmare of meetings and "has your team merged the changes Bob made so we can roll out our new format yet?" We needed 3 teams to roll out changes at the same time!
With protobufs, we were able to make changes to our wire format incredibly rapidly, we had a nice source control managed asset that defined our format, there weren't any confusions as to how data was laid out, and clients using older versions of our definitions just missed out on newer features, nothing actually broke.
It was an insanely large improvement, and honestly for the managed platforms, the performance wasn't appreciably worse than trying to convince Java to read in uint8s and write out uint8s.
My team was working on an embedded platform with RAM measured in kilobytes, and it was worth us eating the overhead just to get rid of the countless meetings we had to hold whenever we made a change to any of our structures.
This is only true if you decide that your storage proto and wire proto are the same. That's not at all necessary (and generally not recommended). More to the point, FieldMask exists in proto, so redaction is fully supported, though I rarely see them used.
The structures that are generated by the protobuf tool are simply there to help with transforming your internal state into the format needed to satisfy the shared contract, allowing your language's compiler to tell you if you have violated that contract. Theoretically you could produce/consume protobufs without them, but they are provided as a convenience so that you can deal with serialization transforms directly in your language of choice instead of banging bytes.
repeated oneof actions {
ActionTypeOne type_one;
ActionTypeTwo type_two;
}
or like: oneof thingy {
repeated string first_option;
string second_option;
}
I can see why you'd want to do either of those things, but it doesn't seem like a huge deal to me to wrap those in another message. oneof thingy {
repeated string first_option;
string second_option;
}
Not the most beautiful solution, but I've seen it in production and it works fine.Perhaps you would prefer something like Ion: https://amzn.github.io/ion-docs/guides/why.html