HTTP/1.X client library, written in Rust
- I/O-free coroutines: every HTTP exchange is exposed as a
resume(arg: Option<&[u8]>)state machine. No sockets, no async runtime, nostdrequired. Run against any blocking, async, or fuzz harness. - Standard, blocking client:
- Light client (requires
clientfeature):HttpClientStd::new(stream)wraps a connectedRead + Writestream and exposessend/send_http10. You still own TCP / TLS. - Full std client (requires
rustls-ring,rustls-aws, ornative-tlsfeature):HttpClientStd::connect(url, tls)openshttp:///https://URLs via pimalaya/stream and returns a ready-to-use client.
- Light client (requires
- HTTP versions: HTTP/1.0 (RFC 1945, fixed-length or read-to-EOF body) and HTTP/1.1 (RFC 9112, fixed-length, chunked, or read-to-EOF body).
- Authentication helpers:
Authorization: Bearer <token>(RFC 6750) andAuthorization: Basic <base64(user:pass)>(RFC 7617). .well-knowndiscovery (RFC 8615) shipped as a dedicated coroutine.
The io-http library is written in Rust, and relies on cargo features to enable or disable functionalities. Default features can be found in the features section of the Cargo.toml, or on docs.rs.
This library implements HTTP as I/O-agnostic coroutines: no sockets, no async runtime, no std required by the protocol layer.
| Module | What it covers |
|---|---|
| 1945 | HTTP/1.0: request/response coroutine (Http10Send) |
| 6750 | OAuth 2.0 Bearer token: Authorization: Bearer <token> |
| 7617 | HTTP Basic authentication: Authorization: Basic <base64(user:pass)> |
| 8615 | .well-known URI discovery: WellKnown coroutine |
| 9110 | HTTP semantics: shared types HttpRequest, HttpResponse, StatusCode |
| 9112 | HTTP/1.1: request/response coroutine (Http11Send), chunked transfer encoding |
io-http can be consumed three ways, depending on how much of the I/O stack you want to own. Each mode is gated by cargo features.
Whichever mode you pick, every coroutine exposes resume(arg: Option<&[u8]>) returning a result enum with four shapes:
WantsRead: caller reads more bytes from the socket and feeds them back on the next call. PassSome(&[])to signal EOF.WantsWrite(Vec<u8>): caller writes these bytes to the socket. The next call typically passesNone.Ok { … }: terminal success.Err { … }: terminal failure.
Http10Send / Http11Send also expose a WantsRedirect { url, response, … } variant; follow the redirect by building a new coroutine against url (possibly on a new connection).
No features required: works in #![no_std], no sockets, no async runtime. You own the loop and the bytes; the library only produces request bytes and consumes server responses.
Send an HTTP/1.1 request against an async Tokio + rustls stack (the same shape works under blocking, fuzzing, or in-memory replay):
use std::sync::Arc;
use io_http::{
rfc9110::request::HttpRequest,
rfc9112::send::{Http11Send, Http11SendResult},
};
use rustls::ClientConfig;
use rustls_platform_verifier::ConfigVerifierExt;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};
use tokio_rustls::TlsConnector;
use url::Url;
#[tokio::main]
async fn main() {
let url = Url::parse("https://example.com/").unwrap();
let domain = url.domain().unwrap().to_owned();
let port = url.port_or_known_default().unwrap_or(443);
let config = Arc::new(ClientConfig::with_platform_verifier().unwrap());
let connector = TlsConnector::from(config);
let server_name = domain.clone().try_into().unwrap();
let tcp = TcpStream::connect((domain.as_str(), port)).await.unwrap();
let mut stream = connector.connect(server_name, tcp).await.unwrap();
let request = HttpRequest::get(url)
.header("Host", &domain)
.header("Connection", "close");
let mut send = Http11Send::new(request);
let mut arg: Option<&[u8]> = None;
let mut buf = [0u8; 4096];
let response = loop {
match send.resume(arg.take()) {
Http11SendResult::Ok { response, .. } => break response,
Http11SendResult::WantsRead => {
let n = stream.read(&mut buf).await.unwrap();
arg = Some(&buf[..n]);
}
Http11SendResult::WantsWrite(bytes) => stream.write_all(&bytes).await.unwrap(),
Http11SendResult::WantsRedirect { url, .. } => panic!("redirect to {url}"),
Http11SendResult::Err(err) => panic!("{err}"),
}
};
println!("{} {}", response.version, *response.status);
}Enable the client feature. HttpClientStd::new(stream) wraps any blocking Read + Write and exposes send / send_http10. You still open the TCP socket and run TLS yourself, and hand over a ready-to-talk stream; the client takes it from there.
[dependencies]
io-http = { version = "0.0.3", default-features = false, features = ["client"] }use std::{net::TcpStream, sync::Arc};
use io_http::{client::HttpClientStd, rfc9110::request::HttpRequest};
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use rustls_platform_verifier::ConfigVerifierExt;
use url::Url;
let url = Url::parse("https://example.com/")?;
let domain = url.domain().unwrap();
let config = ClientConfig::with_platform_verifier()?;
let server_name = domain.to_string().try_into()?;
let conn = ClientConnection::new(Arc::new(config), server_name)?;
let tcp = TcpStream::connect((domain, 443))?;
let stream = StreamOwned::new(conn, tcp);
let mut client = HttpClientStd::new(stream);
let request = HttpRequest::get(url)
.header("Host", domain)
.header("Connection", "close");
let output = client.send(request)?;
println!("{} {}", output.response.version, *output.response.status);Enable one of the TLS feature flags: rustls-ring (default), rustls-aws, or native-tls. HttpClientStd::connect(url, tls) opens http:// (plain TCP) or https:// (implicit TLS) via pimalaya/stream, returning a ready-to-use client.
[dependencies]
io-http = "0.0.3" # rustls-ring is enabled by defaultuse io_http::{client::HttpClientStd, rfc9110::request::HttpRequest};
use pimalaya_stream::tls::Tls;
use url::Url;
let url = Url::parse("https://example.com/")?;
let tls = Tls::default();
let mut client = HttpClientStd::connect(&url, &tls)?;
let request = HttpRequest::get(url.clone())
.header("Host", url.host_str().unwrap())
.header("Connection", "close");
let output = client.send(request)?;
println!("{} {}", output.response.version, *output.response.status);See complete examples at ./examples.
Have a look at projects built on top of this library:
- io-jmap: Set of I/O-free Rust coroutines to manage JMAP sessions
- io-addressbook: Set of I/O-free coroutines to manage contacts
- io-oauth: Set of I/O-free Rust coroutines to manage OAuth flows
- io-starttls: I/O-free Rust coroutine to upgrade any plain stream to a secure one
- Cardamum: CLI to manage contacts
- Ortie: CLI to manage OAuth access tokens
This project is licensed under either of:
at your option.
- Chat on Matrix
- News on Mastodon or RSS
- Mail at pimalaya.org@posteo.net
Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:
- 2022 → 2023: NGI Assure
- 2023 → 2024: NGI Zero Entrust
- 2024 → 2026: NGI Zero Core
- 2027 in preparation…
If you appreciate the project, feel free to donate using one of the following providers:
