Rust: Making a HTTP GET request

Mar 24, 2022

Wwooooowweeeee. I see a theme developing here! Even the seemingly most trivial of tasks if forcing me further down the rabbithole of understanding the Rust-way of doing things. Not that it's necessarily a bad thing, over the long-term, but it definitely front loads a lot of the learning. I'm not even half-way through my usual 14-step process for learning a new language and I've already spent twice as long getting acquainted with Rust as I did Nim.

Today's foray started with having to get familiar with cargo and understanding how dependency management worked. We're going to use the hyper package, which also recommends using tokio for building asynchronous apps. Time to create a Cargo.toml file to define our dependencies:

[dependencies]
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }

Trying to get the basic example working (we'll get there soon!) threw up another error:

error[E0670]: `async fn` is not permitted in Rust 2015
 --> src/main.rs:4:1
  |
4 | async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
  | ^^^^^ to use `async fn`, switch to Rust 2018 or later

And so more digging into Rust editions and more updates to Cargo.toml, which now looks like:

[package]
name = 'http-get'
version = '0.0.1'
edition = "2021"
publish = false

[dependencies]
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }

Turns out though that to use a specific edition via rustc means providing it via the --edition= flag.

Which now gets us to another Rust feature that I already love:

error[E0752]: `main` function is not allowed to be `async`
 --> src/main.rs:4:1
  |
4 | async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`

Some errors have detailed explanations: E0432, E0433, E0752.
For more information about an error, try `rustc --explain E0432`.

And sure enough, running rustc --explain E0432 does indeed give a more detailed and useful explanation of what's wrong! 😍 When you're familiar with a language all of the "helpful" explanatory context can quickly just become noise that buries the actually useful detail. When you're unfamiliar with a particular error though an overly terse output can make it impossible to extract any actionable insight. This feels like such a great way to balance both needs: increase the signal:noise ratio while making additional context available if required.

But... the actual problem was continuing to try and use rustc for testing. cargo run is the way forward to have it use all of my Cargo.toml config.

And finally, we having a working example:

use hyper::Client;
use hyper::body::HttpBody as _;
use tokio::io::{stdout, AsyncWriteExt as _};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let client = Client::new();
    let uri = "http://httpbin.org/ip".parse()?;
    let mut resp = client.get(uri).await?;

    println!("Response: {}", resp.status());
    while let Some(chunk) = resp.body_mut().data().await {
        stdout().write_all(&chunk?).await?;
    }
    Ok(())
}

The #[tokio...] bit is a macro that tells Rust to run the following code on the tokio runtime, which now allows us to define main() as being async. And within it we've now got access to await to make waiting for asynchronous responses easier.

The other notable edition to this code is the introduction of Box in the error path for the Result that main() now returns. This is another case where understanding the memory management of Rust becomes important and it's concepts around ownership and borrowing. By default, most variables/assignments end up on "the stack" which if a LIFO buffer. It's more efficient but it requires a higher degree of certainty around the size or type of data that will be stored in that memory location. The alternative is "the heap" which supports a little more dynamism that the stack, but it only returns a pointer and so is a degree or so removed in terms of the indirection it creates. But sometimes that's what you need. And when you do, Box is how you declare you want your reference to be on the heap and not the stack. Check out the Rust guide on What is Ownership? for more details on the distinctions between the stack and heap.

Hi, I'm Glenn! 👋 I'm currently Director of Product (Terraform) @ HashiCorp, and we're hiring! If you'd like to come and work with me and help make Terraform Cloud even more amazing we have multiple positions opening in Product ManagementDesign, and Engineering & Engineering Management across a range of levels (i.e., junior through to senior). Please send in an application ASAP so we can get in touch.