In an effort to understand the new Rust async/await syntax, I made a super-simple app that simply responds to all HTTP requests with Hello! and deployed on Heroku.

Update: If you just want to create a webservice in Rust and deploy on Heroku, I recommend next blog post: rust on heroku with hyper http. This blog post focuses on the details of how the underlying request and response is handled with async/await, on stable Rust since 11/2019.

The full source code and README instructions can be found on github.com/ultrasaurus/hello-heroku-rust, tokio-only branch

Rust “hello world” app

Make a new project with cargo

cargo new hello_rust --bin
cd hello_rust
git init
git add .
git commit -m “cargo new hello_rust —bin”

cargo run

output:

   Compiling hello_rust v0.1.0 (/Users/sallen/src/rust/hello_rust)
    Finished dev [unoptimized + debuginfo] target(s) in 1.47s
     Running `target/debug/hello_rust`
Hello, world!

Heroku setup

Rust isn’t officially supported by Heroku yet, but there are lots of “buildpacks” which help to deploy a Rust app. I picked emk/heroku-buildpack-rust — most stars, most forks & recently updated!

We need the heroku CLI. I already had it and just did heroku update to sync to latest version (7.35.1). Then to set up the app on heroku:

heroku create --buildpack emk/rust

output provides a unique hostname by default:

Creating app... done, ⬢ peaceful-gorge-05620
Setting buildpack to emk/rust... done
https://peaceful-gorge-05620.herokuapp.com/ | https://git.heroku.com/peaceful-gorge-05620.git

We need a Procfile so heroku knows our entrypoint

echo "web: ./target/release/hello_rust" >> Procfile

Write the app

Add crate dependencies to Cargo.toml and add code to main.rs (and other files as with any Rust app). The emk/rust buildpack takes care of building everything as part of the heroku deploy.

The following lines (in Cargo.toml) will add all of tokio features:

[dependencies]
tokio = { version = "0.2", features = ["full"] }

I’d rather specify only what’s needed, but ran into something I couldn’t debug myself (issue#2050)

The core of the app accepts the sockets connections, but doesn’t read/write:

use std::env;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    // Get the port number to listen on (required for heroku deployment).
    let port = env::var("PORT").unwrap_or_else(|_| "1234".to_string());

    let addr = format!("0.0.0.0:{}", port);
    let mut listener = TcpListener::bind(addr).await.unwrap();

    loop {
        println!("listening on port {}...", port);
        let result = listener.accept().await;
        match result {
            Err(e) => println!("listen.accept() failed, err: {:?}", e),
            Ok(listen) => {
                let (socket, addr) = listen;
                println!("socket connection accepted, {}", addr);
                println!("not doing anything yet");
            }
        }
    }
}

Deploy on heroku

The above code will build and deploy, by simply pushing the code to heroku:

heroku push origin master

We can see what it is doing with heroku logs --tail:

Here’s where it starts the build and then kills the old app:

2020-01-05T03:45:31.000000+00:00 app[api]: Build started by user ...
2020-01-05T03:45:50.450898+00:00 heroku[web.1]: Restarting
2020-01-05T03:45:50.454311+00:00 heroku[web.1]: State changed from up to starting
2020-01-05T03:45:50.244579+00:00 app[api]: Deploy 399e1c85 by user ...
2020-01-05T03:45:50.244579+00:00 app[api]: Release v24 created by user ...
2020-01-05T03:45:50.701533+00:00 heroku[web.1]: Starting process with command `./target/release/hello_rust`
2020-01-05T03:45:51.741040+00:00 heroku[web.1]: Stopping all processes with SIGTERM
2020-01-05T03:45:51.819864+00:00 heroku[web.1]: Process exited with status 143

Oddly, it seems to start the app before “State changed from starting to up” but it will fail if we’re not listening on the right port, so maybe that is as expected:

2020-01-05T03:45:52.343368+00:00 app[web.1]: listening on port 49517...
2020-01-05T03:45:53.322238+00:00 heroku[web.1]: State changed from starting to up
2020-01-05T03:45:53.303486+00:00 app[web.1]: socket connection accepted, 10.171.202.59:17201
2020-01-05T03:45:53.303545+00:00 app[web.1]: not doing anything yet
2020-01-05T03:45:53.303619+00:00 app[web.1]: listening on port 49517...
2020-01-05T03:45:53.313259+00:00 app[web.1]: socket connection accepted, 172.17.146.217:43686
2020-01-05T03:45:53.313285+00:00 app[web.1]: not doing anything yet
2020-01-05T03:45:53.313370+00:00 app[web.1]: listening on port 49517...
2020-01-05T03:46:28.000000+00:00 app[api]: Build succeeded
2020-01-05T03:46:48.251168+00:00 heroku[router]: at=error code=H13 desc="Connection closed without response" method=GET path="/" host=peaceful-gorge-05620.herokuapp.com request_id=a0d630d9-790a-47db-87af-67e680b27907 fwd="69.181.194.59" dyno=web.1 connect=1ms service=1ms status=503 bytes=0 protocol=https

So, the first socket connection above is some internal heroku checker, then when I attempt to go to the app URL in the browser, it fails (as expected).

Async read and write

I tried to keep the code clear with as little magic as possible. It’s a bit verbose (without even handling HTTP in any general way), but I found it helpful to see the details of read and write.

Note that adding use tokio::prelude::*; allows calling of read_line (defined in tokio::io::AsyncBufReadExt) and write_all (defined in tokio::io::AsyncWriteExt).
The additional code reads the bytes from the socket line by line until we get the the end of the HTTP Request (signalled by a blank line). So we look for two CLRFs (one at the end of the last header line and one for the blank line).

tokio::spawn(async move makes it so sure we can read/write from one socket while also listening for additional connections. tokio::spawn will allow the program execution to continue, while concurrently allowing our async function process_socket to read and write from the socket. Because we added #[tokio::main] above our async fn main entry point, tokio will set up an executor which will wait for all of our spawned tasks to complete before exiting.

use std::env;
use tokio::net::TcpListener;
use tokio::prelude::*;

#[tokio::main]
async fn main() {
    // Get the port number to listen on (required for heroku deployment).
    let port = env::var("PORT").unwrap_or_else(|_| "1234".to_string());

    let addr = format!("0.0.0.0:{}", port);
    let mut listener = TcpListener::bind(addr).await.unwrap();

    loop {
        println!("listening on port {}...", port);
        let result = listener.accept().await;
        match result {
            Err(e) => println!("listen.accept() failed, err: {:?}", e),
            Ok(listen) => {
                let (socket, addr) = listen;
                println!("socket connection accepted, {}", addr);
                // Process each socket concurrently.
                tokio::spawn(async move {
                    let mut buffed_socket = tokio::io::BufReader::new(socket);
                    let mut request = String::new();
                    let mut result;
                    loop {
                        result = buffed_socket.read_line(&mut request).await;
                        if let Ok(num_bytes) = result {
                            if num_bytes > 0 && request.len() >= 4 {
                                let end_chars = &request[request.len() - 4..];
                                if end_chars == "\r\n\r\n" {
                                    break;
                                };
                            }
                        }
                    }
                    if let Err(e) = result {
                        println!("failed to read from socket, err: {}", e);
                        return;
                    }
                    let html = "<h1>Hello!</h1>";
                    println!("request: {}", request);
                    let response = format!(
                        "HTTP/1.1 200\r\nContent-Length: {}\r\n\r\n{}",
                        html.len(),
                        html
                    );
                    let write_result = buffed_socket.write_all(response.as_bytes()).await;
                    if let Err(e) = write_result {
                        println!("failed to write, err: {}", e);
                    }
                });
            }
        }
    }
}

Background

Here’s my environment info (rustup show):

stable-x86_64-apple-darwin (default)
rustc 1.39.0 (4560ea788 2019-11-04)

Reference docs

  • https://docs.rs/tokio/0.2.6/tokio/net/struct.TcpListener.html
  • https://docs.rs/tokio/0.2.6/tokio/net/struct.TcpStream.html
  • https://docs.rs/tokio/0.2.6/tokio/task/fn.spawn.html

Learning about HTTP/3… Here are some starter notes

  • Talk by Lucas Pardue at Demuxed 2019 — seek to 08:51:02 in twitch video
  • Another good overview: https://curl.haxx.se/video/curlup-2019/2019-03-31-Robin-Marx-QUIC-details.pdf

Good to know that they are going through a real IETF process so QUIC is evolving. CloudFlare makes it available, and will track the standard, so could be breaking changes ahead!

Rust implementation “Quiche”

uses Rust nightly, here’s a cheat sheet to getting set up..

git clone --recursive git@github.com:cloudflare/quiche.git

# git submodule update --init --recursive

cargo +nightly build --examples
cargo +nightly test

or if you don’t want to keep writing +nightly then:

rustup override set nightly

Learning Rust, like with any new programming language, requires learning the language of compiler error messages, which is partly about the Rust language itself and partly how Rust programmers talk about the language. One of the first error messages that tripped me up was when I was attempting to return a Result.

I wrote a function that had a syntax error like this one:

fn get_num() -> Result<i32, &'static str> {
  let result = 4;
  OK(result);
}

Which generates this error:

   |
11 | fn get_num() -> Result<i32, &'static str> {
   |    -------      ^^^^^^^^^^^^^^^^^^^^^^^^^ expected enum `std::result::Result`, found ()
   |    |
   |    implicitly returns `()` as its body has no tail or `return` expression
12 |   let result = 4;
13 |   OK(result);
   |             - help: consider removing this semicolon
   |
   = note: expected type `std::result::Result<i32, &'static str>`
              found type `()`

So, of course, I remove the semicolon, which leads to this error:

error[E0425]: cannot find function `OK` in this scope
  --> examples/tuple.rs:13:3
   |
13 |   OK(result)
   |   ^^ help: a tuple variant with a similar name exists: `Ok`

The compiler doesn’t know what I’m attempting to do, so it gives me two possibilities:
1. cannot find function OK
2. a tuple variant with a similar name exists: Ok

When I first saw this error, I thought the second part was simply pointing out the location of the error and giving me more detail. I spent a few hours trying to figure out where I had created a conflicting tuple variant… and what the heck was a tuple variant? I copy/pasted seemingly identical code and somehow the problem went away but I didn’t know what I had fixed. When I hit this error a second time, I isolated a small test case, wrote a StackOverflow post and some helpful person pointed out that Ok in Rust has a lower-case k.

For some reason my fingers really want to type OK and my eyes really want to see that as a correct formation of the syntax, so I get this error now and then. “Hello, tuple variant my old friend!” says the voice in my head that anthropomorphizes my code. Then as my fingers automatically fix the typo and recompile, I reflect on Rust tuples and wonder about tuple variants.

So, what’s a tuple?

A tuple is a “finite heterogeneous sequence,” and one of Rust’s primitive type (see doc).

The enum keyword allows us to express a type which has multiple variants which from context must be tuple variants, though I was never able to find a reference to this in the docs.

Update:
discussion on twitter
– bug filed with suggestions from twitter thread: https://github.com/rust-lang/rust/issues/65386
– there are some very awesome people developing the Rust language (and surrounding ecosystem of tools and docs)