Helsing’s internal infrastructure and software platforms make extensive use of protocol buffers and gRPC to define and implement inter-service APIs. While gRPC’s ease of use and performance have served us very well — both in cloud environments and in embedded systems — we have hit roadblocks when deploying gRPC-based services in brownfield networks that lack support for HTTP/2.
To work around such limitations, we have adopted Twirp, a gRPC alternative designed by Twitch. Twirp uses (a subset of) standard gRPC protobuf definitions, but implements a simpler transport protocol compatible with HTTP 1.1.
We recently open-sourced Twurst , Helsing’s Rust implementation of the Twirp protocol. Twurst focuses on versatility and allows implementing servers for both gRPC and Twirp from the same code base. In this blog post, we first explore why we adopted Twirp and then provide minimal code samples for a Twurst server and client. If you read to the end, you may even learn about the etymology.
Twurst is available on GitHub under the Apache 2 license.
Why Twirp?
gRPC relies on HTTP/2, stream transport, and binary data encoding. In working with our customers’ legacy infrastructure, we frequently encounter networks with constraints that are incompatible with gRPC. Such constraints typically exist due to a combination of system age and security or compliance requirements for defence networks; for example:
- Only HTTP 1.1 is supported, no HTTP/2, no WebSockets
- HTTP bodies are fully buffered, no support for streaming HTTP bodies
- Bodies must be in plain text, no binary encoding
- Non-standards HTTP headers get discarded and HTTP trailers are not supported
Instead of butchering our own, customized version of gRPC to follow all these constraints, we chose to adopt Twirp. Indeed, Twirp:
- uses plain HTTP 1.1, clients like Rust reqwest or web browsers
fetchmethod work out of the box. - does not support gRPC
streamAPIs, a constraint we had anyway. - supports protobuf JSON encoding as a first-class citizen. If a request is posted with the
application/jsondatatype, the response should be returned in the same format. Binary protobuf format is also supported for performance. - relies only on the
content-typeheader. Errors are returned as JSON payload with a relevant HTTP status code instead of using custom headers. One could even skip the need for the content-type header with proper sniffing to differentiate JSON and binary protobuf.
Twirp already has more than 20 implementations, providing support in a lot of languages, including the well-known protobuf-ts library.
Twurst
Twurst is not the first Rust implementation of Twirp:
- twirp-rs is maintained by GitHub. Sadly, it has limited JSON support, relying only on Serde on top of the structure generated by Prost leading to non-standard JSON and no support for protobuf well-known types.
- prost-twirp has been abandoned for a few years
This is why we have decided to develop a new Rust Twirp library.
Similar to the other two libraries, Twurst’s design is heavily inspired by Tonic, the most common gRPC implementation in Rust. Like Tonic, Twurst relies on the Prost bindings generator for protobuf. Twurst augments it with the prost-reflect library to add proper JSON support (including for well-known types).
Twurst server
Twurst servers build on Axum, just like Tonic and twirp-rs. Similarly to Tonic, the user first writes a build.rs script to auto-generate Rust code from .proto definitions:
We can then wire up a Twirp service by implementing the generated trait and use Twurst to get an axum::Router. This approach allows to embed the Twirp server into a larger HTTP server and make use of Axum and Tower middleware to handle access control, CORS, logging, etc.
The following code sample defines a Twirp service and serves it with CORS enabled under the /twirp path alongside a /healthz path for health checks:
serve.await
Twurst services also support gRPC. We can spawn a gRPC server alongside a Twirp server in order to smoothly migrate from gRPC to Twirp (note the into_grpc_router instead of into_router):
axum::serve(
tokio::net::TcpListener::bind("localhost:8080").await?,
ExampleServiceServicer {}.into_grpc_router().fallback(grpc_fallback)
).await
In contrast to Tonic or twirp-rs, Twurst supports Axum extractors. For example, to access request headers, we first augment the build script:
.with_axum_request_extractor("headers", "::axum::http::HeaderMap")
and then access headers (eg, AUTHORIZATION) in the the trait implementation:
The extractor mechanism allows, for example, to integrate with a Tower middleware for request authentication.
Twurst client
Like servers, Twurst clients rely on generated code. The build step generates structs that wrap a TwirpHttpClient and let us call a Twirp service with a few lines of code:
;
// ExampleService is the name of the Twirp service
;
// Execute the Twirp request
The TwirpHttpClient is a small wrapper over the tower::Service trait, allowing to customize the transport layer. For example, the following code injects credentials into all Twirp requests:
;
Because TwirpHttpClient wraps tower::Service and axum::Router implements tower::Service, it is easy to build a TwirpHttpClient from a Twirp service implementation; this can be handy for mocking:
let my_mock_client = new;
Testing and compatibility
To test our implementation we first wrote a set of unit tests based on the great Twirp wire protocol specification. On top of these tests, we checked interoperability with two other implementations. We tested our Rust client with the Python server Twirpy and the Rust server with the protobuf-ts TypeScript client. In both cases, we tested the happy path, advanced protobuf features (for example, the JSON encoding of well-known types) and proper error support. We also tested our gRPC server implementation with both protobuf-ts and Tonic clients, ensuring it works properly.
Conclusion
Twurst has been instrumental for Twirp adoption at Helsing and in open-sourcing the code base we hope Twurst will be a useful for the broader Rust and Twirp communities. We look forward to your feedback and/or feature requests on GitHub!
PS: “Twurst” is a freestyle portmanteau of “Twirp” and “Rust”. It is also a play-on-words with a German delicacy, Teewurst.
Author: Thomas PT