#async-http-client #mio #native-tls #async #websocket-client #http

mio_httpc

mio_httpc is a fully async and sync http client running on top of mio only

76 releases

0.10.6 Oct 3, 2024
0.10.5 Feb 22, 2024
0.10.4 Aug 10, 2023
0.10.2 Jan 23, 2023
0.2.3 Dec 18, 2017

#87 in HTTP client


Used in lightql

MIT/Apache

325KB
7.5K SLoC

mio_httpc is an async http client that runs on top of mio only.

For convenience it also provides CallBuilder::exec for a simple one-line blocking HTTP call.

Except CallBuilder::exec no call will block, not even for DNS resolution as it is implemented internally to avoid blocking.

For https to work mio_httpc requires you specify one of the TLS implementations using features: native, openssl or rtls (rustls). Default build will fail on any https URI.

CallBuilder also has URL construction functions (host/path_segm/query/set_https/auth/https) which will take care of url-safe encoding.

mio_httpc does a minimal amount of allocation and in general works with buffers you provide and an internal pool of buffers that get reused on new calls.

Documentation

Chunk transfer decoding

mio_httpc uses a default max chunk size of 128k. If you are writing a web scraper or something similar, this might trip you up since some websites have pretty large chunks. I will not increase (or remove) this default size limitation because I do not like unchecked memory growth from a third party. You can easily increase this with CallBuilder::chunked_max_chunk call.

TODO/FEATURE LIST

  • Basic API
  • Configurable TLS backend
  • Chunked encoding download
  • Chunked encoding upload
  • Safe URL construction
  • Basic Auth
  • Digest Auth
  • Automatic redirects
  • Keep-alive connection pool
  • DNS retries
  • Timeouts
  • Websockets
  • gzip body decoding
  • SSL pinning on subjectPublicKeyInfo (OpenSSL backend with any target_os and macos/ios with native backend)
  • HTTP2
  • Download to file

EXAMPLES

Include mio_httpc in your project with:

# System native TLS implementation
mio_httpc = { version = "0.8", features = ["native"] }
# Openssl
# mio_httpc = { version = "0.8", features = ["openssl"] }
# Rustls
# mio_httpc = { version = "0.8", features = ["rtls"] }

Sync call

extern crate mio_httpc;
use mio_httpc::CallBuilder;
 
 // One line blocking call.
 
 let (response_meta, body) = CallBuilder::get().timeout_ms(5000).url("http://www.example.com")?.exec()?;

 // With URL construction.
 // This way of building the URL is highly recommended as it will always result in correct
 // values by percent encoding any URL unsafe characters.
 // This calls: https://www.example.com/a/b?key1=val1
 let (response_meta, body) = CallBuilder::get()
    .timeout_ms(5000)
    .https()
    .host("www.example.com")
    .path_segm("a")
    .path_segm("b")
    .query("key1","val1")
    .exec()?;
 

Basic async get

cargo run --example get --features "native" -- "https://edition.cnn.com"

// or
cargo run --example get --features "openssl" -- "https://edition.cnn.com"

// or
cargo run --example get --features "rtls" -- "https://edition.cnn.com"
extern crate mio_httpc;
extern crate mio;

use mio_httpc::{CallBuilder,Httpc};
use mio::{Poll,Events};

fn main() {
    let mut poll = Poll::new().unwrap();
    let mut htp = Httpc::new(10,None);
    let args: Vec<String> = ::std::env::args().collect();
    let mut call = CallBuilder::get()
        .url(args[1].as_str()).expect("Can not parse url")
        .timeout_ms(500)
        .call_simple(&mut htp, poll.registry())
        .expect("Call start failed");

    let to = ::std::time::Duration::from_millis(100);
    let mut events = Events::with_capacity(8);
    'outer: loop {
        poll.poll(&mut events, Some(to)).unwrap();
        for cref in htp.timeout().into_iter() {
            if call.is_ref(cref) {
                println!("Request timed out");
                call.abort(&mut htp);
                break 'outer;
            }
        }

        for ev in events.iter() {
            let cref = htp.event(&ev);

            if call.is_call(&cref) {
                if call.perform(&mut htp, poll.registry()).expect("Call failed") {
                    let (resp,body) = call.finish().expect("No response");
                    if let Ok(s) = String::from_utf8(v) {
                        println!("Body: {}",s);
                    }
                    break 'outer;
                }
            }
        }
    }
}

Websockets

cargo run --example ws --features="native" -- "wss://demos.kaazing.com/echo"
extern crate mio_httpc;
extern crate mio;

use mio_httpc::{CallBuilder,Httpc,WebSocket,WSPacket};
use mio::{Poll,Events};
// ws://demos.kaazing.com/echo

fn main() {
    let mut poll = Poll::new().unwrap();
    let mut htp = Httpc::new(10,None);
    let args: Vec<String> = ::std::env::args().collect();

    let mut ws = CallBuilder::get()
        .url(args[1].as_str()).expect("Can not parse url")
        .websocket(&mut htp, poll.registry())
        .expect("Call start failed");

    let to = ::std::time::Duration::from_millis(800);
    'outer: loop {
        let mut events = Events::with_capacity(8);
        poll.poll(&mut events, Some(to)).unwrap();
        for cref in htp.timeout().into_iter() {
            if ws.is_ref(cref) {
                println!("Request timed out");
                break 'outer;
            }
        }

        if events.len() == 0 {
            // ws.ping(None);
            println!("send yo");
            ws.send_text(true, "yo!");
        }

        for ev in events.iter() {
            let cref = htp.event(&ev);

            if ws.is_call(&cref) {
                if ws.is_active() {
                    loop {
                        match ws.recv_packet(&mut htp, poll.registry()).expect("Failed recv") {
                            WSPacket::Pong(_) => {
                                println!("Got pong!");
                            }
                            WSPacket::Ping(_) => {
                                println!("Got ping!");
                                ws.pong(None);
                            }
                            WSPacket::None => {
                                break;
                            }
                            WSPacket::Close(_,_) => {
                                println!("Got close!");
                                ws.close(None, None);
                                break 'outer;
                            }
                            WSPacket::Text(fin,txt) => {
                                println!("Got text={}, fin={}",txt,fin);
                            }
                            WSPacket::Binary(fin,b) => {
                                println!("Got bin={}B, fin={}",b.len(),fin);
                            }
                        }
                    }
                } else {
                    if ws.sendq_len() == 0 {
                        ws.ping(None);
                    }
                }
            }
        }
        // Any ping/pong/close/send_text/send_bin has just been buffered.
        // perform and recv_packet actually send over socket.
        ws.perform(&mut htp, poll.registry()).expect("Call failed");
    }
    ws.perform(&mut htp, poll.registry());
    ws.finish(&mut htp);
}

Dependencies

~5–18MB
~275K SLoC