Announcing Tonic 0.5

July 9, 2021

We are pleased to announce version 0.5 of Tonic, a native gRPC implementation in Rust. 0.5 is a big release and has been in the works for some time.

Some key new features are:

gRPC-Web

gRPC-Web is a protocol that allows clients to connect to gRPC services over HTTP/1.1, instead of the usual HTTP/2. A common use-case for gRPC-Web is JavaScript clients running in the browser. Previously that would require using a proxy to translate the HTTP/2 requests to HTTP/1.1.

However new crate tonic-web allows regular Tonic servers to accept gRPC-Web requests without the need of an external proxy.

Enabling gRPC-Web support is as easy as:

use tonic::transport::Server;
// code generated by tonic-build
use hello_world::greeter_server::{GreeterServer, Greeter};

struct MyGreeter;

#[tonic::async_trait]
impl Greeter for MyGreeter {
    // ...
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "0.0.0.0:3000".parse().unwrap();

    let greeter = GreeterServer::new(MyGreeter);

    // enable grpc-web support for our `greeter` service
    let service = tonic_web::enable(greeter);

    Server::builder()
        // by default, tonic servers only accept http2 requests
        // so we have to enable receiving http1 as well
        .accept_http1(true)
        .add_service(service)
        .serve(addr)
        .await?;

    Ok(())
}

For more details see the tonic-web crate.

Compression

Tonic can now transparently compress and decompress requests, responses, and streams.

Enabling compression on a client is done like so:

let client = GreeterClient::new(channel)
    // compress requests
    .send_gzip()
    /// accept compressed responses
    .accept_gzip();

And like so on the server:

let service = GreeterServer::new(greeter)
    // accept compressed requests
    .accept_gzip()
    // compress responses, if supported by the client
    .send_gzip();

Note this requires enabling the compression feature on both Tonic and tonic-build. See the docs for more details.

Improved Tower integration

Tonic has always had support for extending clients and servers through Tower's Service trait but in 0.5 the new Server::layer method makes it even easier. For example, we can add tracing and authorization to a service by using middleware from tower-http:

use tonic::transport::Server;
use tower_http::{
    auth::RequireAuthorizationLayer,
    trace::TraceLayer,
};

// The stack of middleware our service will be wrapped in
let layer = tower::ServiceBuilder::new()
    // High level tracing of requests and responses
    .layer(TraceLayer::new_for_grpc())
    // Authorize all requests using a token
    .layer(RequireAuthorizationLayer::bearer("my-secret-token"))
    // Convert our `ServiceBuilder` into a `tower::Layer`
    .into_inner();

Server::builder()
    // Apply our middleware stack to the server
    .layer(layer)
    .add_service(GreeterServer::new(MyGreeter)
    .serve(addr)
    .await?;

More flexible interceptors

Interceptors are lightweight middleware that can, among other things, be used to modify the metadata of incoming requests and optionally reject them with a status.

However, Tonic's support for interceptors has always been fairly limited. For example, you couldn't combine multiple interceptors, and would instead have to use Tower's Service abstraction, which is more powerful but also requires more code to set up.

In Tonic 0.5, we have a new interceptor API that is as easy to use as before, but now uses Tower internally. That means interceptors can be applied in the same way as any other Tower middleware. For example adding multiple interceptors to a client is now possible with:

use tonic::{
  Request, Status,
  service::interceptor_fn,
  transport::Endpoint
};

fn intercept_one(req: Request<()>) -> Result<Request<()>, Status> {
    // ...
}

fn intercept_two(req: Request<()>) -> Result<Request<()>, Status> {
    // ...
}

let channel = Endpoint::from_static("http://[::1]:50051").connect_lazy()?;

let intercepted_channel = tower::ServiceBuilder::new()
    .layer(interceptor_fn(intercept_one))
    .layer(interceptor_fn(intercept_two))
    .service(channel);

let client = GreeterClient::new(intercepted_channel);

0.5 includes many other smaller features and improvements. The changelog has all the details.

As always, if you have questions you can find us in #tonic in the Tokio Discord server.

ā€” David Pedersen (@davidpdrsn)