isahc 2.0.0

The practical HTTP client that is fun to use.
Documentation
use isahc::{Request, error::ErrorKind, net::IpVersion, prelude::*};
use std::{
    io::{self, Read, Write},
    net::{Ipv4Addr, Ipv6Addr, Shutdown, TcpListener, TcpStream, ToSocketAddrs},
    thread,
};
use testserver::mock;

#[macro_use]
mod utils;

// Check if the test host supports IPv6.
fn is_ipv6_supported() -> bool {
    if let Ok(addrs) = "localhost:0".to_socket_addrs() {
        for addr in addrs {
            if addr.is_ipv6() {
                return true;
            }
        }
    }

    false
}

#[test]
fn local_addr_returns_expected_address() {
    let m = mock!();

    let response = isahc::get(m.url()).unwrap();

    assert_eq!(m.requests_received(), 1);
    assert_eq!(response.local_addr().unwrap().ip(), Ipv4Addr::LOCALHOST);
    assert!(response.local_addr().unwrap().port() > 0);
}

#[test]
fn remote_addr_returns_expected_address() {
    let m = mock!();

    let response = isahc::get(m.url()).unwrap();

    assert_eq!(m.requests_received(), 1);
    assert_eq!(response.remote_addr(), Some(m.addr()));
}

#[test]
fn local_and_remote_addr_returns_expected_addresses_on_error() {
    let server = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
    let addr = server.local_addr().unwrap();

    thread::spawn(move || {
        let (mut client, _) = server.accept().unwrap();
        client.write_all(b"foobar").unwrap();
        client.flush().unwrap();
    });

    let error = isahc::get(format!("http://localhost:{}", addr.port())).unwrap_err();

    assert_eq!(error.remote_addr(), Some(addr));
    assert_eq!(error.local_addr().unwrap().ip(), Ipv4Addr::LOCALHOST);
    assert!(error.local_addr().unwrap().port() > 0);
}

#[test]
fn ipv4_only_will_not_connect_to_ipv6() {
    if !is_ipv6_supported() {
        eprintln!("skipping test because host does not support IPv6");
        return;
    }

    // Create server on IPv6 only.
    let server = TcpListener::bind((Ipv6Addr::LOCALHOST, 0)).unwrap();
    let port = server.local_addr().unwrap().port();

    let result = Request::get(format!("http://localhost:{}", port))
        .ip_version(IpVersion::V4)
        .body(())
        .unwrap()
        .send();

    assert_matches!(result, Err(e) if e == ErrorKind::ConnectionFailed);
}

#[test]
fn ipv6_only_will_not_connect_to_ipv4() {
    if !is_ipv6_supported() {
        eprintln!("skipping test because host does not support IPv6");
        return;
    }

    // Create server on IPv4 only.
    let server = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
    let port = server.local_addr().unwrap().port();

    let result = Request::get(format!("http://localhost:{}", port))
        .ip_version(IpVersion::V6)
        .body(())
        .unwrap()
        .send();

    assert_matches!(result, Err(e) if e == ErrorKind::ConnectionFailed);
}

#[test]
fn any_ip_version_uses_ipv4_or_ipv6() {
    // Create an IPv4 listener.
    let server_v4 = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
    let port = server_v4.local_addr().unwrap().port();

    // Create an IPv6 listener on the same port.
    let server_v6 = TcpListener::bind((Ipv6Addr::LOCALHOST, port)).unwrap();

    fn respond(client: &mut TcpStream, response: &[u8]) -> io::Result<()> {
        let _ = client.read(&mut [0; 8192])?;
        client.write_all(response)?;
        client.flush()?;
        client.shutdown(Shutdown::Both)
    }

    // Each server response with a string indicating which address family used.
    thread::spawn(move || {
        let (mut client, _) = server_v4.accept().unwrap();
        respond(
            &mut client,
            b"HTTP/1.1 200 OK\r\ncontent-length:4\r\n\r\nipv4",
        )
        .unwrap();
    });
    thread::spawn(move || {
        let (mut client, _) = server_v6.accept().unwrap();
        respond(
            &mut client,
            b"HTTP/1.1 200 OK\r\ncontent-length:4\r\n\r\nipv6",
        )
        .unwrap();
    });

    let mut response = Request::get(format!("http://localhost:{}", port))
        .ip_version(IpVersion::Any)
        .body(())
        .unwrap()
        .send()
        .unwrap();

    let text = response.text().unwrap();

    // On dual stack hosts, this should equal "ipv6", but some environments like
    // WSL2 don't support IPv6 properly.
    assert_matches!(text.as_str(), "ipv4" | "ipv6");

    // Our local address should correspond to the same family as the server.
    if text == "ipv6" {
        assert!(response.local_addr().unwrap().is_ipv6());
    } else {
        assert!(response.local_addr().unwrap().is_ipv4());
    }
}