cHTTP 0.5 and the Journey Ahead

One of my more recent projects that I have been putting a lot of effort into is a Rust HTTP client called cHTTP, which I introduced on this blog over 18 months ago. Here I want to share an update on the direction of the project, and also give some detail on what months of late evenings and weekends produced in version 0.5 just published today.

For more context, if you want to know more about what cHTTP is and why it exists, I encourage you to check out the README.

The Journey So Far

Well, in the first few months following the initial release, I made some minor fixes and improvements. I was using cHTTP in some other project (don't remember what anymore to be honest) and needed it to just work. After a while I had everything I needed implemented, the documentation was so-so, and it indeed just worked.

My key impulse in creating cHTTP was to provide a stream-based API simple enough that anyone, even people new to Rust, could start making HTTP requests in just a few minutes. I feel like I accomplished that goal, as you can make normal requests easily with simple one-liners. This is still a big part of cHTTP, and I intend to keep it that way in the future.

Some time later, I decided that I wanted to broaden the scope of cHTTP; more than just a small libcurl wrapper for making requests, and into a full-featured, well-tested, and fast solution for general HTTP programming. One motivation behind this came from conversations at my workplace around Rust and potential use-cases. We build our software using a network of services connected via HTTP APIs, and we have to handle a lot of traffic. We've been bitten by bugs and questionable defaults in multiple (Java) HTTP clients we've used over the years, and Apache HttpComponents is really the only client we're willing to entrust our business to at this point.

HTTP clients are such critical components for so much software these days, and everyone deserves to have something that is both easy to use, flexible, and has a rock-solid implementation. I've been hard at work for the past few months on bringing cHTTP to that level, and I think it is getting really close.

Nowadays I also recommend the acclaimed reqwest as something aiming for some of the same goals as cHTTP. It appears to be pretty solid with a nice API, though (biased as I am) naturally I prefer cHTTP's design. It also comes down to which engine you want to put your trust in, hyper or cURL. I'm a big fan of Daniel Stenberg's work on curl, but I also appreciate the hyper project and am optimistic about its direction. Besides, a little healthy competition is good for the ecosystem!

First-Class Async

Now let's talk a little more specifically about the 0.5 release. Up front, the biggest new feature is first-class support for the upcoming async/.await syntax. Everywhere where there used to be a blocking API method, there is now also another method with an _async suffix that returns a standard Future, which you can easily .await when inside of an asynchronous block or function.

This wasn't easy to implement, because I decided to also use the latest and greatest of the async ecosystem to re-implement the core of the event loop that drives curl under the hood, and ensure that everything in the loop was non-blocking. I first implemented the async core for version 0.2, but left some things synchronous or not implemented optimally. Partially because I knew that this day would come anyway, and partially because designing the async body handlers was really, really tricky to get right!

So what does this look like to users of the API? Well, simple and unsurprising:

// As of this writing, gotta have nightly to do this particular example.
#![feature(async_await)]
// We do need an executor to run our own futures, but this cute little guy is
// good enough for us.
use futures::executor::block_on;
// Import some traits that help make the API super slick.
use chttp::prelude::*;

// Main might return an error.
fn main() -> Result<(), chttp::Error> {
    // Prepare an asynchronous context, and then block the main thread until it
    // finishes.
    block_on(async {
        // Ahh, that's hot!
        let mut response = chttp::get_async("https://example.org").await?;

        // Streaming body is async too? Pinch me, I'm dreaming!
        println!("{}", response.text_async().await?);

        // Or I could have dropped `response` to stop downloading the body,
        // because that's your natural assumption.

        Ok(())
    })
}

I'd love to talk your ear off on how this works under the hood, but I'm afraid of being long-winded (uh, I mean, more than I already am), so here's the reader's digest: At the core, we are driving a single curl multi handle that executes all of a client's active requests concurrently. We drive curl in a singular background thread (the “agent thread”), which communicates to user threads using message passing integrated into the loop. New messages and I/O streaming makes sure the agent thread isn't sleeping on the job by using a specialized waker implemented as the self-pipe trick using UDP loopback. Cool, right!?

I want to pause here for just a second and congratulate everyone who's been working on bringing the asynchronous design to Rust, you've all done a phenomenal job! I spent a good chunk of my life in college studying and implementing advanced asynchronous event models in multiple languages, and I think Rust's Future trait is honestly the best design I've ever seen considering the zero-cost overhead. Using the “notorious” Wakers actually helped me solve some of the tricky problems with curl's body handling. I'd keep gushing about this, but I'll save it for another post later. Maybe I can put my fool's knowledge to use and try to explain in human terms what makes the design so great?

I also want to make note that these async methods are an additional feature. You don't always need async in every program, and that's perfectly fine. The normal, synchronous API will always be a first-class thing in cHTTP if that is what you need. Feel free to use one or the other (or both) as you see fit.

API Ergonomics

One thing I am quite pleased about is how little cHTTP's public API has changed since that initial 0.1 release. The latest API isn't quite compatible with that first API, but it is pretty close and follows the same general structure. Here's an example from the 0.1.0 README:

let mut response = chttp::get("https://example.org").unwrap();
let body = response.body_mut().text().unwrap();
println!("{}", body);

Here's what it looks like today:

use chttp::prelude::*;

let mut response = chttp::get("https://example.org")?;
println!("{}", response.text()?);

Still pretty familiar, but slightly more concise by providing a couple extension methods on the response. The simple one-off API has always been great. But for the more advanced case?

// 0.4.5
use chttp::{self, http, Options};
use std::time::Duration;

let request = http::Request::get("https://example.org")
    .extension(Options::default()
        // Set a 5 second timeout.
        .with_timeout(Some(Duration::from_secs(5))))
    .body(())?;
let response = chttp::send(request)?;

Eh, not so much. The use of extension() here is a little confusing unless you're really familiar with how we use http extensions, and the Options struct was kinda rough to work with. It was also an all-or-nothing thing; either you had to provide a whole Options (which took precedence over everything in the client's default Options), or nothing.

I knew we could do better, so in 0.5 request configuration is handled entirely differently. Configuration is now fine-grained (setting just a timeout on a request overrides just that particular setting in the client) and uses conventional builder methods:

// 0.5.0
use chttp::prelude::*;
use std::time::Duration;

let response = Request::get("https://example.org")
    // Set a 5 second timeout.
    .timeout(Duration::from_secs(5))
    .body(())?
    .send()?;

The way this works is by including a RequestBuilderExt trait in the prelude, which defines extra methods for building configuration and is implemented on the normal http::request::Builder type. There's a few more improvements I'd like to make in this area, but overall I think this is a really solid approach that feels great.

The Journey Ahead

Looking ahead, my goal is to have version 1.0 ready by the end of this year. There are several big things that come to mind that need to be done before it is ready:

After 1.0 is released, I intend for it to have a long, boring (stable) project life. There are of course couple big features I'd like to add eventually:

You can of course check out the project issue tracker for an exhaustive list.

It may take a while for these features to get added if I can't find any additional contributors, as I won't be putting as much time into the project afterward. I have other big projects that I want to focus on instead (but don't worry, I'll still be actively maintaining cHTTP for a long time).

Goodnight

I'm awful at writing conclusions. Write a comment below, shoot me an email, or open a GitHub issue if you have questions, ideas, or accusations of heresy. And of course give cHTTP a try, I hope you like it. ;)

Comments