http-request 22.0.11

http-request is a lightweight, efficient library for building, sending, and handling HTTP/HTTPS requests in Rust applications. It provides a simple and intuitive API, allowing developers to easily interact with web services, whether they use the "HTTP" or "HTTPS" protocol. The library supports various HTTP methods, custom headers, request bodies, timeout, automatic handling of redirects (including detecting redirect loops), and enhanced response body decoding (both automatic and manual), enabling fast and secure communication. Whether working with secure "HTTPS" connections or standard "HTTP" requests, the library is optimized for performance, minimal resource usage, and easy integration into Rust projects.
Documentation
use super::*;

#[test]
fn split_multi_byte_preserves_non_overlapping_segments() {
    assert_eq!(
        split_multi_byte(b"left::middle::right", b"::"),
        vec![
            b"left".as_slice(),
            b"middle".as_slice(),
            b"right".as_slice()
        ]
    );
    assert_eq!(
        split_multi_byte(b"::left::::", b"::"),
        vec![b"".as_slice(), b"left".as_slice(), b"".as_slice()]
    );
}

#[test]
fn split_multi_byte_skips_overlapping_matches() {
    assert_eq!(
        split_multi_byte(b"aaaaa", b"aa"),
        vec![b"".as_slice(), b"".as_slice(), b"a".as_slice()]
    );
}

#[test]
fn split_multi_byte_handles_empty_delimiters_and_inputs() {
    assert_eq!(
        split_multi_byte(b"payload", b""),
        vec![b"payload".as_slice()]
    );
    assert!(split_multi_byte(b"", b"::").is_empty());
    assert_eq!(split_multi_byte(b"one", b"longer"), vec![b"one".as_slice()]);
}

#[test]
fn split_http_head_preserves_body_leading_crlf() {
    assert_eq!(
        split_multi_byte(b"HTTP/1.1 200 OK\r\n\r\n\r\n", b"\r\n\r\n"),
        vec![b"HTTP/1.1 200 OK".as_slice(), b"\r\n".as_slice()]
    );
}

#[test]
fn whitespace_split_ignores_empty_space_and_tab_runs() {
    assert_eq!(
        split_whitespace(b" \tGET\t /path  HTTP/1.1 \t"),
        vec![
            b"GET".as_slice(),
            b"/path".as_slice(),
            b"HTTP/1.1".as_slice()
        ]
    );
    assert!(split_whitespace(b" \t ").is_empty());
    assert!(split_whitespace(b"").is_empty());
}

#[test]
fn wire_request_builder_keeps_binary_body_and_delimiters() {
    let request: Vec<u8> = build_http_request(
        "POST",
        "/upload".to_owned(),
        b"Content-Length: 3\r\n".to_vec(),
        Some(vec![0, 255, 13]),
        "HTTP/1.1".to_owned(),
    );
    assert_eq!(
        request,
        b"POST /upload HTTP/1.1\r\nContent-Length: 3\r\n\r\n\0\xff\r"
    );
    let empty: Vec<u8> = build_http_request(
        "GET",
        "/".to_owned(),
        Vec::new(),
        None,
        "HTTP/1.1".to_owned(),
    );
    assert_eq!(empty, b"GET / HTTP/1.1\r\n\r\n");
}

#[test]
fn line_delimiter_search_respects_offsets_and_partial_input() {
    let data: &[u8] = b"one\r\n\r\ntwo\r\n";
    assert_eq!(find_crlf(data, 0), Some(3));
    assert_eq!(find_crlf(data, 5), Some(5));
    assert_eq!(find_double_crlf(data, 0), Some(3));
    assert_eq!(find_double_crlf(data, 4), None);
    assert_eq!(find_crlf(data, data.len()), None);
    assert_eq!(find_double_crlf(b"\r\n\r", 0), None);
}

#[test]
fn case_insensitive_search_handles_binary_and_empty_patterns() {
    assert_eq!(
        find_pattern_case_insensitive(b"\xffContent-TYPE: bin", b"content-type:"),
        Some(1)
    );
    assert_eq!(find_pattern_case_insensitive(b"abAB", b"AB"), Some(0));
    assert_eq!(find_pattern_case_insensitive(b"short", b"longer"), None);
    assert_eq!(find_pattern_case_insensitive(b"body", b""), None);
}

#[test]
fn chunked_body_parser_decodes_extensions_and_binary_payloads() {
    assert_eq!(
        parse_chunked_body(b"3;name=value\r\na\0\xff\r\n2\r\nbc\r\n0\r\n\r\n").unwrap(),
        b"a\0\xffbc"
    );
    assert!(parse_chunked_body(b"2\r\nok\r\n4\r\nno").is_err());
    assert!(parse_chunked_body(b"invalid\r\nbytes").is_err());
}

#[test]
fn response_header_helpers_accept_case_insensitive_wire_names() {
    let headers: &[u8] =
        b"HTTP/1.1 200 OK\r\nCoNtEnT-LeNgTh: 12\r\nTransfer-Encoding: Chunked\r\n\r\n";
    assert_eq!(get_content_length(headers).unwrap(), 12);
    assert!(is_chunked_encoding(headers).unwrap());
    assert_eq!(get_content_length(b"HTTP/1.1 200 OK\r\n\r\n").unwrap(), 0);
    assert!(!is_chunked_encoding(b"Content-Length: 0\r\n").unwrap());
}

#[test]
fn decimal_and_status_parsers_cover_valid_and_invalid_ascii() {
    assert!(parse_decimal_bytes(b" \t123rest").is_err());
    assert_eq!(parse_decimal_bytes(b"0007").unwrap(), 7);
    assert!(parse_decimal_bytes(b"x123").is_err());
    assert!(parse_decimal_bytes(b"").is_err());
    assert_eq!(parse_status_code(b"204"), 204);
    for invalid in [
        b"".as_slice(),
        b"20".as_slice(),
        b"2000".as_slice(),
        b"2x0".as_slice(),
    ] {
        assert_eq!(parse_status_code(invalid), 0);
    }
}

#[test]
fn response_buffer_growth_covers_each_capacity_branch() {
    assert_eq!(calculate_buffer_capacity(b"data", 4, 8), 0);
    assert_eq!(calculate_buffer_capacity(b"data", 1, 0), 1024);
    assert_eq!(calculate_buffer_capacity(b"data", 8, 8), 16);
    assert_eq!(calculate_buffer_capacity(b"data", 20, 8), 36);
}

#[test]
fn response_header_parser_extracts_redirect_length_and_chunked_state() {
    let mut length: usize = 0;
    let mut redirect: Option<Vec<u8>> = None;
    let mut chunked: bool = false;
    parse_response_headers(
        b"HTTP/1.1 302 Found\r\nLocation:/next\r\nContent-Length: 4\r\n\r\n",
        b"http/1.1",
        b"location:",
        &mut length,
        &mut redirect,
        &mut chunked,
    )
    .unwrap();
    assert_eq!(length, 4);
    assert_eq!(redirect, Some(b"/next".to_vec()));
    assert!(!chunked);
    parse_response_headers(
        b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n",
        b"http/1.1",
        b"location:",
        &mut length,
        &mut redirect,
        &mut chunked,
    )
    .unwrap();
    assert_eq!(length, 0);
    assert!(redirect.is_none());
    assert!(chunked);
}

#[test]
fn response_header_lookup_ignores_prefixed_names_and_body_fields() {
    for response in [
        b"HTTP/1.1 200 OK\r\nX-Content-Length: 999\r\nX-Transfer-Encoding: chunked\r\n\r\n"
            .as_slice(),
        b"HTTP/1.1 200 OK\r\nX-Note: Content-Length: 999; Transfer-Encoding: chunked\r\n\r\n"
            .as_slice(),
        b"HTTP/1.1 200 OK\r\n\r\nContent-Length: 999\r\nTransfer-Encoding: chunked\r\n".as_slice(),
    ] {
        assert_eq!(get_content_length(response).unwrap(), 0);
        assert!(!is_chunked_encoding(response).unwrap());
    }
    assert_eq!(
        get_content_length(
            b"HTTP/1.1 200 OK\r\nX-Content-Length: 999\r\nContent-Length: 5\r\n\r\n"
        )
        .unwrap(),
        5
    );
}

#[test]
fn response_content_length_rejects_non_decimal_values() {
    for value in ["5x", "+5", "-5", "0x5", "", "5 5", "5\t5", "5, 5", "\x005"] {
        let headers: String = format!("HTTP/1.1 200 OK\r\nContent-Length: {value}\r\n\r\n");
        assert!(get_content_length(headers.as_bytes()).is_err(), "{value:?}");
    }
    assert_eq!(
        get_content_length(b"Content-Length:\t 0005 \t\r\n\r\n").unwrap(),
        5
    );
}

#[test]
fn response_content_length_rejects_overflow_without_panicking() {
    let overflow: String = format!("{}0", usize::MAX);
    let headers: String = format!("HTTP/1.1 200 OK\r\nContent-Length: {overflow}\r\n\r\n");
    assert!(matches!(
        parse_decimal_bytes(overflow.as_bytes()),
        Err(RequestError::ContentLengthTooLarge(HttpStatus::BadRequest))
    ));
    assert!(matches!(
        get_content_length(headers.as_bytes()),
        Err(RequestError::ContentLengthTooLarge(HttpStatus::BadRequest))
    ));
    let maximum: String = format!("Content-Length: {}\r\n\r\n", usize::MAX);
    assert!(matches!(
        get_content_length(maximum.as_bytes()),
        Err(RequestError::ContentLengthTooLarge(HttpStatus::BadRequest))
    ));
}

#[test]
fn response_framing_rejects_transfer_encoding_with_content_length() {
    for headers in [
        b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n".as_slice(),
        b"HTTP/1.1 200 OK\r\nTRANSFER-ENCODING: CHUNKED\r\ncontent-length: 5\r\n\r\n".as_slice(),
    ] {
        let mut length: usize = 0;
        let mut redirect: Option<Vec<u8>> = None;
        let mut chunked: bool = false;
        assert!(
            parse_response_headers(
                headers,
                b"http/1.1",
                b"location:",
                &mut length,
                &mut redirect,
                &mut chunked
            )
            .is_err()
        );
    }
}

#[test]
fn response_framing_rejects_unsupported_transfer_codings() {
    for value in [
        "gzip",
        "chunked, gzip",
        "gzip, chunked",
        "xchunked",
        "chunkedx",
        "chunked;foo=bar",
        "",
        "identity",
    ] {
        let headers: String = format!("HTTP/1.1 200 OK\r\nTransfer-Encoding: {value}\r\n\r\n");
        assert!(
            is_chunked_encoding(headers.as_bytes()).is_err(),
            "{value:?}"
        );
    }
    assert!(is_chunked_encoding(b"Transfer-Encoding:\t ChUnKeD \t\r\n\r\n").unwrap());
}

#[test]
fn response_framing_rejects_duplicate_length_and_transfer_fields() {
    for headers in [
        b"HTTP/1.1 200 OK\r\nContent-Length: 1\r\ncontent-length: 2\r\n\r\n".as_slice(),
        b"HTTP/1.1 200 OK\r\nContent-Length: 1\r\nContent-Length: 1\r\n\r\n".as_slice(),
        b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\ntransfer-encoding: chunked\r\n\r\n"
            .as_slice(),
    ] {
        let mut length: usize = 0;
        let mut redirect: Option<Vec<u8>> = None;
        let mut chunked: bool = false;
        assert!(
            parse_response_headers(
                headers,
                b"http/1.1",
                b"location:",
                &mut length,
                &mut redirect,
                &mut chunked
            )
            .is_err()
        );
    }
}

#[test]
fn response_headers_reject_invalid_names_and_control_values() {
    for field in [
        "Content-Length : 5",
        " Content-Length: 5",
        "\tContent-Length: 5",
        "X-Test: a\x00b",
        "X-Test: a\x7fb",
        "X-Test: a\rb",
        "X-Test: a\nb",
    ] {
        let headers: String = format!("HTTP/1.1 200 OK\r\n{field}\r\n\r\n");
        assert!(get_content_length(headers.as_bytes()).is_err(), "{field:?}");
    }
}

#[test]
fn chunked_body_rejects_invalid_or_excessive_chunk_sizes() {
    for size in [
        "+1",
        "-1",
        "0x1",
        " 1",
        "1 ",
        "1\t",
        "g",
        "",
        "ffffffffffffffffffffffffffffffff",
        "ffffffffffffffff",
        "4000001",
    ] {
        let body: String = format!("{size}\r\nx\r\n0\r\n\r\n");
        assert!(chunked_body_end(body.as_bytes()).is_err(), "{size:?}");
        assert!(parse_chunked_body(body.as_bytes()).is_err(), "{size:?}");
    }
    assert_eq!(parse_chunk_size(b"aBc;name=value").unwrap(), 0xabc);
}

#[test]
fn chunked_body_requires_data_and_final_trailer_delimiters() {
    for body in [
        b"1\r\nxXX0\r\n\r\n".as_slice(),
        b"1\r\nx\r\n0\r\n".as_slice(),
        b"1\r\nx\r\n0\r\nX-Trailer: value\r\n".as_slice(),
    ] {
        assert!(parse_chunked_body(body).is_err());
    }
    assert_eq!(chunked_body_end(b"0\r\n").unwrap(), None);
    assert_eq!(chunked_body_end(b"0\r\n\r\n").unwrap(), Some(5));
    let body: &[u8] = b"1\r\nx\r\n0\r\nX-Trailer: value\r\n\r\n";
    assert_eq!(chunked_body_end(body).unwrap(), Some(body.len()));
    assert_eq!(parse_chunked_body(body).unwrap(), b"x");
}

#[test]
fn response_redirect_uses_exact_location_header() {
    let mut length: usize = 0;
    let mut redirect: Option<Vec<u8>> = None;
    let mut chunked: bool = false;
    parse_response_headers(b"HTTP/1.1 302 Found\r\nX-Location: http://evil.test/\r\nLocation: http://safe.test/\r\nContent-Length: 0\r\n\r\n", b"http/1.1", b"location:", &mut length, &mut redirect, &mut chunked).unwrap();
    assert_eq!(redirect, Some(b"http://safe.test/".to_vec()));
}