Creating a reqwest Request without a builder

After a lot of reading and practicing exercises, I'm now writing my first real-world Rust program, a testing utility for a website search engine.

I've started out prototyping with reqwests. The easy way to do this is using the RequestBuilder, but I'm a TDD guy and would like to write my tests without out-of-process dependencies. I've come up with the code below (not a test, just rewritten as simply as possible) for exercising an endpoint. The main thing I struggled with is setting the body.

Any suggestions and criticism are welcome!

use reqwest::Url;
use reqwest::blocking::{Body, Request};

fn main() {
    let params = vec![("teeth", "pointy"), ("venom", "deadly")];
    let url = Url::parse_with_params("http://reptiles.com", params).unwrap();
    let mut request = Request::new(reqwest::Method::POST, url);
    let body = request.body_mut();
    *body = Some(Body::from("The body content".to_string()));
}

RequestBuilder is still part of reqwest, what's "out-of-process" about it?

1 Like

True, it is. The builder version looks like this:

impl TryFrom<TestCase> for Request {
    type Error = Error;

    fn try_from(value: TestCase) -> Result<Self, Error> {
        let params = vec![
            ("limit", value.limit.to_string()),
            ("query", value.query),
            ("language", value.uri_language),
            ("restrict", value.restrict),
            ("matchpartial", value.match_partial),
        ];

        Client::new()
            .post(value.url.as_str())
            .query(&params)
            .json(&value.selected_languages)
            .build()
    }
}

But then I need another client to actually execute it:

let request = Request::try_from(test_case).unwrap();
let response = Client::new().execute(request).unwrap();

I think normally you'd call send(). That would be out of process. I could implement TryFrom for RequestBuilder instead, test that and then call build().send(). That would be quite acceptable really.

One thing I'm not sure I got right in the code above is this:

let body = request.body_mut();
*body = Some(Body::from("The body content".to_string()));

I'm still new at this, and wasn't entirely sure how to handle that part. It certainly seems less ergonomic than the RequestBuilder version. I just saw a type hint for mut &Option<Body> and hacked it.

Maybe you could pass the Client as part of TestCase? Then you wouldn't have to construct a new one in TryFrom::try_from. Storing the client as a global variable in a LazyLock would also be an option.

* is Rust's dereference operator. Your snippet does update the body of the request. I agree with your observation that RequestBuilder offers the nicer API though.

It would be better to double-down on using RequestBuilder, and return unbuilt RequestBuilder to the caller, instead of a finalized Request.

BTW: you're saying out-of-process, but it's unclear what you mean by that. For the meaning of process as an operating system process (running executable), it all stays within a single process.

3 Likes

The trait implementation might be a bit overkill. In my case it might be better to have a SearchClient with a method that produces RequestBuilders from TestCases.

Cool. Well now I know a new trick.

Thanks!

1 Like

Sure. The documentation implies the same thing:

You should prefer to use the RequestBuilder and RequestBuilder::send()

That was a fancy way of saying I don’t want to connect to the web host in my unit tests. :blush:

For those who are interested, here's the refactored code:

impl From<TestCase> for RequestBuilder {
    fn from(value: TestCase) -> RequestBuilder {
        let params = vec![
            ("limit", value.limit.to_string()),
            ("query", value.query),
            ("language", value.uri_language),
            ("restrict", value.restrict),
            ("matchpartial", value.match_partial),
        ];

        Client::new()
            .post(value.url.as_str())
            .query(&params)
            .json(&value.selected_languages)
    }
}

I'm happy with the overhead of creating a new Client each time and I can use the builder either via send():

fn main() {
    let test_case = TestCase {
        query: String::from("adze"),
        selected_languages: vec!["en".to_string(), "pli".to_string()],
        ..Default::default()
    };

    let request = RequestBuilder::from(test_case);
    let response = request.send().unwrap();
    let results: SearchResults = response.json().unwrap();
    println!("Total = {}", results.total)
}

Or using build() in my unit tests to inspect the Request:

    #[test]
    fn search_request_has_correct_url() {
        let test_case = TestCase {
            query: String::from("adze"),
            selected_languages: vec!["en".to_string(), "pli".to_string()],
            ..Default::default()
        };

        let request = RequestBuilder::from(test_case).build().unwrap();

        assert_eq!(
            request.url().as_str(),
            "http://localhost/api/search/instant?limit=1&query=adze&language=en&restrict=all&matchpartial=false"
        );
    }

Happy hacking!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.