Asynchronous Agnostic HTTP Client: my first crate

Hi everyone! I just published my first crate, aahc, the Asynchronous Agnostic HTTP Client.

I was inspired to write this when I was looking at the existing asynchronous HTTP client crates and realized that, as far as I can tell, only Isahc is actually genuinely executor-agnostic (but achieves this by spawning a separate thread to do all the I/O), while everything else either only works with Tokio, or only works with async-std, or uses feature flags to choose either Tokio or async-std, or something similar, but does not work with just any executor at all. By externalizing socket creation, aahc is executor agnostic: as long as you hand it a socket, which can be anything that implements futures_io::AsyncBufRead and futures_io::AsyncWrite, it’ll send an HTTP request over it. Tokio, async-std, Glib, presumably Rustls, any kind of socket your personal choice of executor provides ought to work, with no auxiliary threads. To top it off, aahc is also 100% safe, makes very few heap allocations (and even they can be disabled), and has a very small deptree (only futures-core, futures-io, and httparse).

I’d appreciate comments on anything, large or small—choice of approach, architecture, code details, metadata, documentation/comments, etc.. While I pointed the repository entry in the metadata at my Gitlab site because I prefer Gitlab, the code is also on Github here; if you want to file a ticket either is fine.

Thanks everyone!

1 Like

Async-std's futures can work no any runtime so clients like async-h1 are also executor agnostic. Still, cool project!

Cool project!

Though note that there are libraries that are truly agnostic. For example, hyper falls in this category, and async-h1 might also, although I don't know for sure. To be fair, both of these has a clear preference for an executor, but I know that fully replacing the executor is possible with hyper.

@Kestrer I strongly disagree with the notion that silently using another runtime in the background is being executor agnostic.

1 Like

That's fair, at least it doesn't behave differently on different executors, even if it's less efficient on some.

Ah, yes, you’re right about hyper being executor-agnostic. I just remembered I was also inspired by Smoke-testing Rust HTTP clients to try and make something with minimal unsafety and much lighter weight, so I discounted the ones that depended on the http crate.

async-h1’s Cargo.toml appears to declare an unconditional dependency on async-std.

Yes, but so does hyper on Tokio. Hyper does this for the IO traits (plus it has defaults for Tokio's tcp stream and such), and indeed async-h1 takes an AsyncRead + AsyncWrite using async-std's traits.

Again, I do not actually know whether async-h1 can be used in a manner such that async-std is only imported for its traits, but that is the case for hyper, and it may also be for async-h1.

From looking at the source, it looks like async-h1 uses some helper functions and that sort of thing from async-std as well. It might be the case that they’re all actually usable in other executors, but that doesn’t seem to be the sort of thing that gets documented thoroughly (e.g. it seems obvious that async_std::io::copy should work under any executor because all it needs to do is operate on the passed-in readable and writeable, but it’s not actually documented as such, so it would be legal for it to get optimized in an executor-specific way some day even if it isn’t now). Ah well, I suppose I have contributed to proving XKCD 927 if nothing else.