isahc 1.8.1

The practical HTTP client that is fun to use.
Documentation
use futures_lite::future::block_on;
use isahc::{prelude::*, HttpClient, Request};
use std::{
    io::{self, Write},
    net::{Shutdown, TcpListener, TcpStream},
    thread,
    time::Duration,
};
use testserver::mock;

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

    isahc::get(m.url()).unwrap();

    m.request().expect_header("accept", "*/*");
    m.request()
        .expect_header("accept-encoding", "deflate, gzip");
}

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

    isahc::get(m.url()).unwrap();

    m.request()
        .expect_header_regex("user-agent", r"^curl/\S+ isahc/\S+$");
}

// Issue [#209](https://github.com/sagebind/isahc/issues/209)
#[test]
fn setting_an_empty_header_sends_a_header_with_no_value() {
    let m = mock!();

    Request::get(m.url())
        .header("an-empty-header", "")
        .body(())
        .unwrap()
        .send()
        .unwrap();

    m.request().expect_header("an-empty-header", "");
}

// Issue [#209](https://github.com/sagebind/isahc/issues/209)
#[test]
fn setting_a_blank_header_sends_a_header_with_no_value() {
    let m = mock!();

    Request::get(m.url())
        .header("an-empty-header", "    ")
        .body(())
        .unwrap()
        .send()
        .unwrap();

    m.request().expect_header("an-empty-header", "");
}

// Issue [#190](https://github.com/sagebind/isahc/issues/190)
#[test]
fn override_client_default_user_agent() {
    let m = mock!();

    let client = HttpClient::builder()
        .default_header("user-agent", "foo")
        .build()
        .unwrap();

    client.get(m.url()).unwrap();

    m.request().expect_header("user-agent", "foo");
}

// Issue [#205](https://github.com/sagebind/isahc/issues/205)
#[test]
fn set_title_case_headers_to_true() {
    let m = mock!();

    let client = HttpClient::builder()
        .default_header("foo-BAR", "baz")
        .title_case_headers(true)
        .build()
        .unwrap();

    client.get(m.url()).unwrap();

    assert_eq!(m.request().method(), "GET");
    m.request().expect_header("Foo-Bar", "baz");
}

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

    let client = HttpClient::builder()
        .default_header("X-header", "some-value1")
        .build()
        .unwrap();

    let request = Request::builder()
        .method("GET")
        .uri(m.url())
        .body(())
        .unwrap();

    let _ = client.send(request).unwrap();

    m.request().expect_header("accept", "*/*");
    m.request()
        .expect_header("accept-encoding", "deflate, gzip");
    m.request().expect_header("X-header", "some-value1");
}

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

    let client = HttpClient::builder()
        .default_header("X-header", "some-value1")
        .build()
        .unwrap();

    let request = Request::builder()
        .method("GET")
        .header("X-header", "some-value2")
        .uri(m.url())
        .body(())
        .unwrap();

    let _ = client.send(request).unwrap();

    m.request().expect_header("accept", "*/*");
    m.request()
        .expect_header("accept-encoding", "deflate, gzip");
    m.request().expect_header("X-header", "some-value2");
}

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

    let client = HttpClient::builder()
        .default_header("X-header", "some-value1")
        .default_header("X-header", "some-value2")
        .build()
        .unwrap();

    let request = Request::builder()
        .method("GET")
        .uri(m.url())
        .body(())
        .unwrap();

    let _ = client.send(request).unwrap();

    m.request().expect_header("accept", "*/*");
    m.request()
        .expect_header("accept-encoding", "deflate, gzip");
    // Both values should be present.
    m.request().expect_header("X-header", "some-value1");
    m.request().expect_header("X-header", "some-value2");
}

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

    let client = HttpClient::builder()
        .default_header("X-header", "some-value1")
        .default_header("X-header", "some-value2")
        .build()
        .unwrap();

    let request = Request::builder()
        .method("GET")
        .header("X-header", "some-value3")
        .uri(m.url())
        .body(())
        .unwrap();

    let _ = client.send(request).unwrap();

    m.request().expect_header("accept", "*/*");
    m.request()
        .expect_header("accept-encoding", "deflate, gzip");
    m.request().expect_header("X-header", "some-value3");
}

#[test]
fn trailer_headers() {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let url = format!("http://{}", listener.local_addr().unwrap());

    thread::spawn(move || {
        let mut stream = listener.accept().unwrap().0;

        consume_request_in_background(&stream);

        stream
            .write_all(
                b"\
            HTTP/1.1 200 OK\r\n\
            transfer-encoding: chunked\r\n\
            trailer: foo\r\n\
            \r\n\
            2\r\n\
            OK\r\n\
            0\r\n\
            foo: bar\r\n\
            \r\n\
        ",
            )
            .unwrap();

        let _ = stream.shutdown(Shutdown::Write);
    });

    let mut body = None;
    let response = isahc::get(url).unwrap().map(|b| {
        body = Some(b);
    });

    thread::spawn(move || {
        io::copy(body.as_mut().unwrap(), &mut io::sink()).unwrap();
    });

    assert_eq!(response.trailer().wait().get("foo").unwrap(), "bar");
}

#[test]
fn trailer_headers_async() {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let url = format!("http://{}", listener.local_addr().unwrap());

    thread::spawn(move || {
        let mut stream = listener.accept().unwrap().0;

        consume_request_in_background(&stream);

        stream
            .write_all(
                b"\
            HTTP/1.1 200 OK\r\n\
            transfer-encoding: chunked\r\n\
            trailer: foo\r\n\
            \r\n\
            2\r\n\
            OK\r\n\
            0\r\n\
            foo: bar\r\n\
            \r\n\
            ",
            )
            .unwrap();

        let _ = stream.shutdown(Shutdown::Write);
    });

    block_on(async move {
        let mut body = None;
        let response = isahc::get_async(url).await.unwrap().map(|b| {
            body = Some(b);
        });

        thread::spawn(move || {
            block_on(async move {
                futures_lite::io::copy(body.as_mut().unwrap(), &mut futures_lite::io::sink())
                    .await
                    .unwrap();
            })
        });

        assert_eq!(
            response.trailer().wait_async().await.get("foo").unwrap(),
            "bar"
        );
    });
}

#[test]
fn trailer_headers_timeout() {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let url = format!("http://{}", listener.local_addr().unwrap());

    thread::spawn(move || {
        let mut stream = listener.accept().unwrap().0;
        stream.set_nodelay(true).unwrap();

        consume_request_in_background(&stream);

        stream
            .write_all(
                b"\
            HTTP/1.1 200 OK\r\n\
            transfer-encoding: chunked\r\n\
            trailer: foo\r\n\
            \r\n",
            )
            .unwrap();

        for _ in 0..1000 {
            stream.write_all(b"5\r\nhello\r\n").unwrap();
        }

        stream.write_all(b"0\r\n").unwrap();

        thread::sleep(Duration::from_millis(200));

        stream.write_all(b"foo: bar\r\n\r\n").unwrap();

        let _ = stream.shutdown(Shutdown::Write);
    });

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

    // Since we don't consume the response body and the trailer is in a separate
    // packet from the header, we won't receive the trailer in time.
    assert!(
        response
            .trailer()
            .wait_timeout(Duration::from_millis(10))
            .is_none()
    );
}

fn consume_request_in_background(stream: &TcpStream) {
    let mut stream = stream.try_clone().unwrap();

    thread::spawn(move || {
        let _ = io::copy(&mut stream, &mut io::sink());
        let _ = stream.shutdown(Shutdown::Read);
    });
}