httpman 0.1.5

A fast, modern HTTP client with tab management and request persistence
Documentation
use futures::StreamExt;
use reqwest::redirect::Policy;
use tokio::io::AsyncWriteExt;
use ulid::Ulid;

use crate::models::{HttpMethod, Request, Response, ResponseBody};

pub async fn handle_http_request(request: Request) -> anyhow::Result<Response> {
    let client = reqwest::Client::builder()
        .redirect(match request.follow_redirects {
            true => Policy::default(),
            false => Policy::none(),
        })
        .build()?;
    let mut req = match request.method {
        HttpMethod::Get => client.get(&request.url),
        HttpMethod::Post => client.post(&request.url),
        HttpMethod::Put => client.put(&request.url),
    };
    for (k, v) in request.headers.iter() {
        req = req.header(k, v);
    }

    // send req
    let http_res = req.send().await?;
    let mut headers = http_res
        .headers()
        .iter()
        .map(|(k, v)| {
            (
                k.as_str().to_string(),
                v.to_str().unwrap_or_default().to_string(),
            )
        })
        .collect::<Vec<_>>();
    headers.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));

    let status_code = http_res.status().as_u16();

    let body = match http_res.headers().get("content-type") {
        None => flush_to_disk(http_res)
            .await?
            .map(ResponseBody::Binary)
            .unwrap_or_else(|| ResponseBody::Empty),
        Some(content_type) => {
            let ct = content_type.to_str().unwrap_or_default();
            if ct.contains("text") {
                let body = http_res.text().await?;
                ResponseBody::Text(body)
            } else if ct.contains("json") {
                let body = http_res.text().await?;
                serde_json::from_str::<serde_json::Value>(&body)
                    .map(|value| ResponseBody::Json {
                        value,
                        length: body.len(),
                    })
                    .unwrap_or(ResponseBody::Text(body))
            } else if ct.contains("image") {
                flush_to_disk(http_res)
                    .await?
                    .map(ResponseBody::Image)
                    .unwrap_or_else(|| ResponseBody::Empty)
            } else if http_res.content_length().unwrap_or_default() == 0 {
                ResponseBody::Empty
            } else {
                flush_to_disk(http_res)
                    .await?
                    .map(ResponseBody::Binary)
                    .unwrap_or_else(|| ResponseBody::Empty)
            }
        }
    };

    Ok(Response {
        status_code,
        headers,
        body,
    })
}

async fn flush_to_disk(
    http_res: reqwest::Response,
) -> Result<Option<std::path::PathBuf>, anyhow::Error> {
    let tmp_file = std::env::temp_dir().join(format!("httpman_res_{}", Ulid::new().to_string()));
    let mut file = tokio::fs::File::create(tmp_file.clone()).await?;
    let mut stream = http_res.bytes_stream();
    let mut bytes_written = false;
    while let Some(chunk) = stream.next().await {
        let chunk = chunk?;
        file.write(&chunk).await?;
        bytes_written = true;
    }

    if !bytes_written {
        std::fs::remove_file(&tmp_file)?;
        return Ok(None);
    }
    Ok(Some(tmp_file))
}