DEV Community

Cover image for My Tiny Rust Utils, Part 4: web.rs
icsboyx
icsboyx

Posted on

My Tiny Rust Utils, Part 4: web.rs

I like reqwest.

I just do not always like seeing raw reqwest patterns repeated all over my application code.

That is the whole reason src/utils/web.rs exists.

I don't need full HTTP framework. I just need one place to keep a few recurring things:

  • response decoding
  • basic header preparation
  • a default User-Agent
  • a simpler response type for the rest of the app

A smaller response model

The file starts here:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ResponseBody {
    Json(Value),
    Text(String),
    Binary(Bytes),
}

#[derive(Debug, Clone)]
pub struct HttpReply {
    pub status: StatusCode,
    pub headers: HeaderMap,
    pub body: ResponseBody,
}
Enter fullscreen mode Exit fullscreen mode

That already helps me.

Instead of passing raw reqwest::Response deeper into the app, I get a smaller local type that matches how this project actually thinks about HTTP replies.

That is enough abstraction for me. No more is needed here.

json_try_into

The helper I probably use the most is this one:

impl ResponseBody {
    pub fn json_try_into<T>(&self) -> Result<T>
    where
        T: for<'de> Deserialize<'de>,
    {
        match self {
            ResponseBody::Json(json) => serde_json::from_value(json.clone()).map_err(Into::into),
            _ => bail!("Response body is not JSON, cannot deserialize"),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

That lets me write things like:

let token = http_reply.body.json_try_into::<BotIdentity>()?;
Enter fullscreen mode Exit fullscreen mode

I like this because it is strict in a simple, readable way.

If the body is not JSON, it says so.

If the JSON shape does not match the target type, it says so.

That is exactly the level of honesty I want from utility code.

Default headers without fuss

The file also takes care of the default User-Agent:

pub const DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

fn prepare_headers(headers: Option<HeaderMap>, user_agent: Option<&str>) -> Result<HeaderMap> {
    let mut headers = headers.unwrap_or_default();

    if !headers.contains_key(header::USER_AGENT) {
        let value = HeaderValue::from_str(user_agent.unwrap_or(DEFAULT_USER_AGENT))?;
        headers.insert(header::USER_AGENT, value);
    }

    Ok(headers)
}
Enter fullscreen mode Exit fullscreen mode

This is a tiny thing, but it is one of those details that becomes annoying if I have to remember it in every call site.

So I would rather centralize it once and stop thinking about it.

The public API stays small

The actual entry points are just these:

pub async fn get_http(
    url: impl AsRef<str>,
    headers: Option<HeaderMap>,
    query: Option<&[(&str, &str)]>,
    user_agent: Option<&str>,
) -> Result<HttpReply> {
    let headers = prepare_headers(headers, user_agent)?;
    let request = http_client().get(url.as_ref()).headers(headers);
    let response = apply_query(request, query).send().await?;

    decode_response(response).await
}

pub async fn post_http(
    url: impl AsRef<str>,
    headers: Option<HeaderMap>,
    query: Option<&[(&str, &str)]>,
    body: Option<Value>,
    user_agent: Option<&str>,
) -> Result<HttpReply> {
    let headers = prepare_headers(headers, user_agent)?;
    let mut request = http_client().post(url.as_ref()).headers(headers);
    if let Some(body) = body {
        request = request.json(&body);
    }
    let response = apply_query(request, query).send().await?;

    decode_response(response).await
}
Enter fullscreen mode Exit fullscreen mode

I am happy when a utility module stays about this size.

It does not try to be clever.

It just absorbs repetition so the rest of the code stays calmer.

Where it pays off

A real call site looks like this:

let sub_reply: Value = post_http(url, Some(headers), None, Some(subscription_request.json_value()?), None)
    .await?
    .body
    .json_try_into()?;
Enter fullscreen mode Exit fullscreen mode

I still recognize that as ordinary application code.

That is important to me.

If the wrapper made this harder to read, I would not consider it a win.

Moving to Part 5

At this point the pieces are all on the table:

  • macros
  • helpers
  • save/load
  • HTTP

The last part is the simplest file, mod.rs, but it is also where I can finally say what this whole crate means to me.

Top comments (0)