Skip to content

pimalaya/io-http

I/O HTTP Documentation Matrix Mastodon

HTTP/1.X client library, written in Rust

Table of contents

Features

  • I/O-free coroutines: every HTTP exchange is exposed as a resume(arg: Option<&[u8]>) state machine. No sockets, no async runtime, no std required. Run against any blocking, async, or fuzz harness.
  • Standard, blocking client:
    • Light client (requires client feature): HttpClientStd::new(stream) wraps a connected Read + Write stream and exposes send / send_http10. You still own TCP / TLS.
    • Full std client (requires rustls-ring, rustls-aws, or native-tls feature): HttpClientStd::connect(url, tls) opens http:// / https:// URLs via pimalaya/stream and returns a ready-to-use client.
  • 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) and Authorization: Basic <base64(user:pass)> (RFC 7617).
  • .well-known discovery (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.

RFC coverage

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

Examples

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. Pass Some(&[]) to signal EOF.
  • WantsWrite(Vec<u8>): caller writes these bytes to the socket. The next call typically passes None.
  • 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).

As a no-std coroutine library

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);
}

As a light std client (BYO stream)

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);

As a full std client (TCP + TLS)

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 default
use 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.

More 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

License

This project is licensed under either of:

at your option.

Social

Sponsoring

nlnet

Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:

If you appreciate the project, feel free to donate using one of the following providers:

GitHub Ko-fi Buy Me a Coffee Liberapay thanks.dev PayPal

Contributors