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” Waker
s 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:
- Benchmarks and optimizations: I guarantee you that I made a few dumb mistakes in hot code paths somewhere, and I want to make sure to smooth out any potential performance issues. My gut tells me that its pretty darn fast, but that’s not a very objective measurement! We need both performance tests and some benchmarks we can run to demonstrate cHTTP’s performance.
- HTTP crate stability: It would be greatly preferable if we could see the http crate be stabilized this year, since it’s a significant piece of cHTTP’s public API. At the very least, we must wait for version 0.2 to deliver to get some really helpful improvements (like
TryFrom
impls and by-value builders). - Tweaks to trait APIs: There are a few changes yet that I’d like to make to the cHTTP traits, but most of them need the things being delivered in version 0.2 of the http crate. So we have to wait for that. I may spend some time contributing to that project in order to expedite the process if I can.
- Project rename: I know it will be a bit of a pain, but “cHTTP” just doesn’t roll off the tongue, you know? Renaming libraries can always be rough, but I think the project needs a much more memorable and fun name. I have one or two names that I will be sharing soon on this issue, but feel free to add a comment if you have suggestions! Get ready for something cheesy!
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:
- Better redirect handling. The underlying curl logic is OK, but it would be neat to let you provide an arbitrary function as the redirect policy.
- An ergonomic API for creating POST forms, because doing it by hand is so darn tedious.
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. ;)
0 comments
Let me know what you think in the comments below. Remember to keep it civil!
Subscribe to this thread