leyline-http 0.1.0

HTTP client that mimics browsers on the wire
Documentation
use std::sync::Arc;

use super::{Prepared, TransportBody, TransportResponse, adopt, check_framing, status};
use crate::core::body::{Body, BodyKind};
use crate::core::error::{Error, Kind, Result};
use crate::core::response::HttpVersion;
use crate::core::session::decompress::BodyLimit;
use crate::pool::h1::h1err_to_io;
use crate::pool::{H1Body, H1PooledError, H1ResponseBody, H1Target, Pool};
use crate::tls::FingerprintConnector;

fn body_to_h1(body: Body) -> H1Body {
    match body.0 {
        BodyKind::Empty => H1Body::Empty,
        BodyKind::Bytes(b) => H1Body::Buffered(b),
        BodyKind::Stream {
            stream,
            length_hint: Some(length),
        } => H1Body::FixedStream { stream, length },
        BodyKind::Stream {
            stream,
            length_hint: None,
        } => H1Body::ChunkedStream { stream },
    }
}

#[tracing::instrument(
    name = "transport.h1",
    level = "debug",
    skip_all,
    fields(
        http.method = req.method,
        http.scheme = req.url.scheme(),
        http.host = req.url.host_str().unwrap_or(""),
    )
)]
pub(crate) async fn send_request_h1(
    pool: &Arc<Pool>,
    connector: &FingerprintConnector,
    req: Prepared<'_>,
) -> Result<TransportResponse> {
    let Prepared {
        method,
        url,
        headers,
        body,
        proxy,
        stream_response,
    } = req;
    check_framing(&headers)?;
    let host = url
        .host_str()
        .ok_or_else(|| Error::new(Kind::Config).with_message("no host in URL"))?;
    let port = url.port_or_known_default().ok_or_else(|| {
        Error::new(Kind::Config)
            .with_message(format!("no default port for scheme {}", url.scheme()))
    })?;
    let scheme = url.scheme();

    let (target, headers) = match (scheme, proxy) {
        ("http", Some(proxy_url)) => {
            let parsed = url::Url::parse(proxy_url).map_err(|e| {
                Error::new(Kind::Config).with_message(format!("invalid proxy URL: {e}"))
            })?;
            let mut headers = headers;
            headers.retain(|(k, _)| !k.eq_ignore_ascii_case("priority"));
            if !matches!(parsed.scheme(), "http" | "https") {
                (H1Target::OriginForm, headers)
            } else {
                if let Some(credentials) = crate::util::proxy_basic_auth(&parsed) {
                    headers.push((
                        "Proxy-Authorization".into(),
                        std::borrow::Cow::Owned(credentials),
                    ));
                }
                (H1Target::AbsoluteForm, headers)
            }
        }
        _ => {
            let mut headers = headers;
            headers.retain(|(k, _)| !k.eq_ignore_ascii_case("priority"));
            (H1Target::OriginForm, headers)
        }
    };

    let h1_body = body_to_h1(body);

    let resp = crate::pool::send_request_h1_pooled(
        pool,
        connector,
        scheme,
        host,
        port,
        method,
        url,
        headers
            .into_iter()
            .map(|(k, v)| (k.into_owned(), v.into_owned()))
            .collect(),
        h1_body,
        proxy,
        target,
        stream_response,
    )
    .await
    .map_err(h1_error_to_core)?;

    let transport_body = match resp.body {
        H1ResponseBody::Buffered(b) => TransportBody::Buffered(b),
        H1ResponseBody::Streaming(s) => TransportBody::Streaming(s),
    };

    Ok(TransportResponse {
        status: status(resp.status)?,
        headers: adopt(resp.headers),
        trailers: Vec::new(),
        body: transport_body,
        final_url: url.clone(),
        version: HttpVersion::Http1_1,
        tls: resp.tls,
        timing: resp.timing,
    })
}

pub(crate) fn h1_error_to_core(e: H1PooledError) -> Error {
    match e {
        H1PooledError::Config(m) => Error::new(Kind::Config).with_message(m),
        H1PooledError::Tls(error) => Error::from(error),
        H1PooledError::Io(io) => match BodyLimit::of_io(&io) {
            Some(limit) => limit.error(),
            None => Error::new(Kind::Io).with_source(io),
        },
        H1PooledError::RequestBody(io) => Error::from_request_body(io),
        H1PooledError::NotResendable(error) => error,
        other => Error::new(Kind::Io).with_source(h1err_to_io(other)),
    }
}