Rust: Making a HTTP GET request | Glenn Gillen
Rust: Making a HTTP GET request

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've spent most of my career working with or at startups. I'm currently the Director of Product @ Ockam where I'm helping developers build applications and systems that are secure-by-design. It's time we started securely connecting apps, not networks.

Previously I led the Terraform product team @ HashiCorp, where we launched Terraform Cloud and set the stage for a successful IPO. Prior to that I was part of the Startup Team @ AWS, and earlier still an early employee @ Heroku. I've also invested in a couple of dozen early stage startups.