Skip to main content

Crate io_http

Crate io_http 

Source
Expand description

§I/O HTTP Documentation Matrix Mastodon

HTTP/1.X client library, written in Rust

This library is composed of 3 feature-gated layers:

  • Low-level I/O-free coroutines: these no_std-compatible state machines contain the whole HTTP/1.X logic and can be used anywhere
  • Mid-level light client: a standard, blocking HTTP client using a Stream: Read + Write
  • High-level full client: light client + TCP connections and TLS negotiations handled for you

§Table of contents

§Features

  • I/O-free coroutines: no_std state machines; no sockets, no async runtime, no std required, drive against any blocking, async, or fuzz harness.
  • Light standard, blocking client (requires client feature)
  • Full standard, blocking client with TLS support:
    • Rustls with ring crypto (requires rustls-ring feature)
    • Rustls with aws crypto (requires rustls-aws feature)
    • Native TLS (requires native-tls feature)
  • HTTP versions: HTTP/1.0 (RFC 1945) and HTTP/1.1 (RFC 9112) with fixed-length, chunked, and read-to-EOF body strategies.
  • Streaming: chunked decoder (Http11ReadChunksStream) + W3C Server-Sent Events parser (SseFrameParser) + std-blocking driver (HttpClientStd::send_streaming).
  • 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.

[!TIP] I/O HTTP is written in Rust and uses cargo features to gate backend support. The default feature set is declared in Cargo.toml or on docs.rs.

§RFC coverage

ModuleWhat it covers
1945HTTP/1.0: request/response coroutine (Http10Send)
6750OAuth 2.0 Bearer token: Authorization: Bearer <token>
7617HTTP Basic authentication: Authorization: Basic <base64(user:pass)>
8615.well-known URI discovery: WellKnown coroutine
9110HTTP semantics: shared types HttpRequest, HttpResponse, StatusCode, HttpSendOutput, HttpSendYield
9112HTTP/1.1: request/response (Http11Send), response-head (Http11ReadHeaders), chunked transfer (whole-body + streaming)
sseW3C Server-Sent Events frame parser (HTML Living Standard)

§Usage

I/O-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 implements the HttpCoroutine trait. Its resume(arg: Option<&[u8]>) method returns a HttpCoroutineState<Yield, Return> with two variants:

  • Yielded(Y): intermediate. Y is the per-coroutine yield type (e.g. HttpYield::WantsRead / HttpYield::WantsWrite(Vec<u8>) for most coroutines; HttpSendYield::WantsRedirect { … } for Http*Send; SseFrameParserYield::Frame(SseFrame) for the SSE parser).
  • Complete(R): terminal. By convention R = Result<Output, Error> carrying the operation’s final value.

§Coroutine

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::{
    coroutine::*,
    rfc9110::{request::HttpRequest, send::HttpSendYield},
    rfc9112::send::Http11Send,
};
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()) {
            HttpCoroutineState::Complete(Ok(out)) => break out.response,
            HttpCoroutineState::Complete(Err(err)) => panic!("{err}"),
            HttpCoroutineState::Yielded(HttpSendYield::WantsRead) => {
                let n = stream.read(&mut buf).await.unwrap();
                arg = Some(&buf[..n]);
            }
            HttpCoroutineState::Yielded(HttpSendYield::WantsWrite(bytes)) => {
                stream.write_all(&bytes).await.unwrap();
            }
            HttpCoroutineState::Yielded(HttpSendYield::WantsRedirect { url, .. }) => {
                panic!("redirect to {url}");
            }
        }
    };

    println!("{} {}", response.version, *response.status);
}

§Light client

Enable the client feature. HttpClientStd::new(stream) wraps any blocking Read + Write and exposes send / send_http10 / send_streaming. You still open the TCP socket and run TLS yourself, then hand over a ready-to-talk stream; the client takes it from there.

[dependencies]
io-http = { version = "0.1.1", 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.clone())
    .header("Host", domain)
    .header("Connection", "close");

let output = client.send(request)?;
println!("{} {}", output.response.version, *output.response.status);

§Full client

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.1.1" # 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);

§Examples

See complete examples at ./examples.

Have also a look at real-world 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

§AI disclosure

This project is developed with AI assistance. This section documents how, so users and downstream packagers can make informed decisions.

  • Tools: Claude Code (Anthropic), Opus 4.7, invoked locally with a persistent project-scoped memory and a small set of repo-specific rules.

  • Used for: Refactors, mechanical multi-file edits, boilerplate (feature gates, error enums, derive macros, trait impls), test scaffolding, doc polish, exploratory design conversations.

  • Not used for: Engineering, critical code, git manipulation (commit, merge, rebase…), real-world tests.

  • Verification: Every AI-assisted change is read, compiled, tested, and formatted before commit (nix develop --command cargo check / cargo test / cargo fmt). Behavioural correctness is verified against the relevant RFC or upstream spec, not assumed from the model output. Tests are never adjusted to fit AI-generated code; the code is adjusted to fit correct behaviour.

  • Limitations: AI models occasionally produce code that compiles and passes tests but is subtly wrong: off-by-one errors, missed edge cases, plausible but nonexistent APIs, stale RFC references. The verification workflow catches most of this; it does not catch all of it. Bug reports are welcome and taken seriously.

  • Last reviewed: 30/05/2026

§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

Modules§

clientclient
Standard, blocking HTTP/1.X client wrapping a boxed Read + Write + Send stream. Each HttpClientStd::send / HttpClientStd::send_http10 is self-contained (HTTP has no session context). With a TLS feature enabled, HttpClientStd::connect opens http:// / https:// URLs end-to-end via pimalaya_stream::std::stream::StreamStd.
coroutine
Generator-shape coroutine driver mirroring core::ops::Coroutine: Yield associated type for intermediate progress, Return for terminal output, and a two-variant HttpCoroutineState (Yielded / Complete).
rfc1945
HTTP/1.0 message syntax (RFC 1945).
rfc6750
OAuth 2.0 Bearer token usage (RFC 6750).
rfc7617
HTTP Basic authentication scheme (RFC 7617).
rfc8615
Well-Known URIs (RFC 8615).
rfc9110
HTTP semantics (RFC 9110).
rfc9112
HTTP/1.1 message syntax (RFC 9112).
sse
W3C Server-Sent Events client.

Macros§

http_try
Coroutine ?: forwards Yielded (via Into), short-circuits on Err (via Into), evaluates to the inner Ok value.