Jun 01 2018

hyper v0.12

Today sees the release of hyper v0.12.0, a fast and correct HTTP library for the Rust language.

This release adds support for several new features, while taking the opportunity to fix some annoyances, and improve the extreme speeds! Look, some wild bullet points appeared:

  • Faster!
  • More correct.
  • Embraces the http crate types.
  • Adds HTTP2 support to both the client and server.
  • The Client and Server are easier to setup and use.
  • Better runtime support.
  • Better body streams.

Faster

hyper 0.11 is already one of the fastest HTTP libraries out there. However, the original server API based around ServerProto prevented it from going full speed. While a new API with a faster dispatcher has existed for a little while now, 0.12 is able to remove that slower way completely, and just default everyone to hyperspeed. But it doesn’t stop there.

Switching the headers types has meant a noticeable boost in serializing of HTTP headers, skipping std::fmt overhead, and removing the need for replacing newlines in header values, thanks to HeaderValue preventing those bytes entirely. While replacing the serialization pathway, further optimizations of checking for semantically important headers is able to be done while serializing, reducing hashmap lookups.

By taking control of the trait used to represent bodies , implementations of Payload can provide hints such as if the body is empty, or exactly how big it thinks it is. This allows some more optimizations, and some API niceties, explained later on in this article.

HTTP Correctness

Being fast is important, but being correct is critical.

There’s quite a few edge cases in the HTTP/1 protocol, and an implementation that you trust to use should make sure to handle all those cases. Browsing various other HTTP/1 implementations, you’ll find that some of the faster ones ignore them, leaving it up to the users to protect themselves manually. Some of the edge cases can mean security vulnerabilities (like not sanitizing newlines out of headers resulting in message splitting), others just mean clients cannot understand you.

hyper’s correct handling of HTTP/1 continues to improve, all while getting faster. For instance, consider some of these things an HTTP implementation should handle:

  • Receiving a Request with Content-Length: 100 and Transfer-Encoding: chunked means it must be chunked encoded.
  • The reverse is also true: if you insert both a Content-Length header and Transfer-Encoding: chunked header, perhaps in different parts of your code, the following body must be chunked.
  • When responding to a CONNECT request, 200 OK responses cannot have bodies. However, responses with other status codes can!
  • If you send a Content-Length: 100 header, but then try to send a body of 150 bytes, the recipient may try to parse bytes 101-150 as a new message.

There’s plenty more, spelled out in RFC7230, and hyper tries to repair any that it finds, or provides an error if there are no repairs that can be done, instead of silently ignoring and having incorrect state compared to the peer. Thanks to hyper’s usage in the Conduit proxy, we continue to find and fix more and more!

HTTP2

There is now built-in support for both the client and server to make use HTTP2.

  • The Server by default will handle both HTTP/1 and HTTP2 connections, and can be configured to only accept HTTP2 if desired.
  • The Client requires explicit configuration for now, enabling a requirement for “prior knowledge” usage of HTTP2. Work is happening to allow for ALPN support to get “automatic” HTTP/1 + HTTP2 usage.

Streaming

As mentioned above, hyper changed the way it describes its streaming bodies. Before, it was just a futures::Stream, but that meant the only thing hyper knew about it was that it could produce some data. Now, hyper defines a Payload trait, which besides streaming data, can also stream trailers, declare its length, and say when it is finished.

By owning the Payload trait, hyper can grow its capabilities when needed, such as to add HTTP2 push promises, or other new features.

The Data type of payloads changed from have AsRef<[u8]> bounds to Buf instead. This importantly allows for custom application implementations to return data chunks that may be from different contiguous memory sources, taking advantage of hyper’s automatic writev support, meaning less copies!

Ergonomics

Using both clients and servers has gotten much easier. There is default support for the new Tokio runtime, removing a lot of boilerplate from getting an event loop and reactor up and running.

Look how simple it is to get a naive HTTP proxy working:

let addr = ([0, 0, 0, 0], 3000).into();

let client = Client::new();
let new_svc = move || {
    let client = client.clone();
    service_fn(move |req|{
        client.request(req)
    })
};

let server = Server::bind(&addr)
    .serve(new_svc)
    .map_err(|e| eprintln!("server error: {}", e));

hyper::rt::run(server);

Of course, the example above doesn’t actually do all the things a proxy should do, but it shows off how simple it is to create a client and server.

These easy builders are enabled by default via the runtime Cargo feature. Importantly, this means that if you already have some other sort of runtime, hyper can still integrate! Disabling the runtime feature removes the dependency on Tokio, though you will need to use the configuration to explain how your runtime works to hyper.

Errors

There has been confusion with hyper’s usage of errors in 0.11 that 0.12 cleans up. Before, both Service::Error and Stream::Error were required to be hyper::Error. However, it wasn’t clear what that really meant, and picking how to create a hyper::Error was equally confusing.

Now, any place that a user would return an error to hyper, it has been changed to just have the bounds Into<Box<std::error::Error>>. This means you can return any custom application error type that implements std::error::Error. hyper doesn’t particularly do anything special with those errors, besides some logging and trying to pass them back up to a higher level error handler, but it’s at least easier to determine what error to return: whatever you want to describe the failure you encountered!

The documentation around Service::Error has been clarified with this reminder as well: servers usually shouldn’t return an error back to hyper in most cases, and instead should return a Response with an appropriate 400 or 500 status code. Returning an error in a server will signal to hyper that it should abort the connection immediately.

Thanks!

Thanks to all those who helped get us this far! Whether it’s through writing code, diagnosing bugs, discussing design and issues, running pre-release versions, it’s all what gets our community to a place with such awesome tools. Thank you.

As a wrap up, some links if you want to see more:

  • #hyper
  • #rust
  • #rust-lang
  • #http