TLS and Compression support for Rust GraphQL server

2020-03-01

Why

In the previous post we created a minimal graphql server in Rust, with juniper and hyper-rs libraries. The implementation was lacking HTTP/2 and response compression support. Hyper-rs server supports HTTP/2 by default, but most HTTP/2 clients require a TLS connection, that's why the browsers will fallback to HTTP/1.1.

How

Enabling Compression

We'll go with the simple route and use a blocking approach. It's good enough, since juniper execution is blocking, and it returns a concrete response, there isn't any streaming or async processing involved. We can use the flate2 library.

cargo add flate2

And wire it up directly in graphql handler.

use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::Write;

async fn handle(req: Request<Body>) -> Result<Response<Body>, Infallible> {
    let mut response = Response::new(Body::empty());

    match (req.method(), req.uri().path()) {
        (&Method::GET, "/") => {
            *response.body_mut() = Body::from("Home");
        }
        (&Method::GET, "/playground") => {
            *response.body_mut() =
                Body::from(juniper::http::playground::playground_source("/graphql"));
            (*response.headers_mut()).append(
                "Content-Type",
                HeaderValue::from_static("text/html;charset=utf-8"),
            );
        }
        (&Method::POST, "/graphql") => {
            let body = hyper::body::to_bytes(req)
                .await
                .expect("to be able to read the request body");

            let query: juniper::http::GraphQLRequest =
                serde_json::from_slice(&body)
                    .expect("to be able to deserialize the json request");

            let graphql_response = query.execute(&SCHEMA, &STATE);
            let status = if graphql_response.is_ok() {
                StatusCode::OK
            } else {
                StatusCode::BAD_REQUEST
            };

            // Create the encoder, write the serialized response as bytes
            let mut e = GzEncoder::new(Vec::new(), Compression::default());
            e.write_all(
                serde_json::to_string(&graphql_response)
                    .expect("to be able to serialize the response").as_bytes(),
            ).unwrap();

            // set the Content-Encoding header
            (*response.headers_mut()).append(
                "Content-Encoding",
                HeaderValue::from_static("gzip"),
            );

            let response_body = e.finish()
                .expect("to be able to compress the response");

            *response.status_mut() = status;
            *response.body_mut() = Body::from(response_body);
        }
        _ => *response.status_mut() = StatusCode::NOT_FOUND,
    }

    Ok(response)
}

Configuring TLS

To add TLS support we'll want to include native_tls and tokio_tls libraries.

cargo add native_tls
cargo add tokio_tls

We'll want to generate a self-signed certificate. This can be done easily with openssl.

openssl req -x509 -newkey rsa:4096 -keyout server.pem -out server.pem -days 365 -nodes
openssl pkcs12 -export -out server_certificate.p12 -inkey server.pem -in server.pem

Conclusions

Configuring TLS Hyper is not as straightforward as in warp-rs, but still doable.