Applied: Simple HTTP Server

Let's use async/.await to build an echo server!

To start, run rustup update stable to make sure you've got stable Rust 1.39 or newer. Once you've done that, run cargo new async-await-echo to create a new project, and open up the resulting async-await-echo folder.

Let's add some dependencies to the Cargo.toml file:

[dependencies]
# The latest version of the "futures" library, which has lots of utilities
# for writing async code. Enable the "compat" feature to include the
# functions for using futures 0.3 and async/await with the Hyper library,
# which use futures 0.1.
futures = { version = "0.3", features = ["compat"] }

# Hyper is an asynchronous HTTP library. We'll use it to power our HTTP
# server and to make HTTP requests.
hyper = "0.12.9"

Now that we've got our dependencies out of the way, let's start writing some code. We have some imports to add:


#![allow(unused_variables)]
fn main() {
use {
    hyper::{
        // Miscellaneous types from Hyper for working with HTTP.
        Body, Client, Request, Response, Server, Uri,

        // This function turns a closure which returns a future into an
        // implementation of the the Hyper `Service` trait, which is an
        // asynchronous function from a generic `Request` to a `Response`.
        service::service_fn,

        // A function which runs a future to completion using the Hyper runtime.
        rt::run,
    },
    futures::{
        // Extension trait for futures 0.1 futures, adding the `.compat()` method
        // which allows us to use `.await` on 0.1 futures.
        compat::Future01CompatExt,
        // Extension traits providing additional methods on futures.
        // `FutureExt` adds methods that work for all futures, whereas
        // `TryFutureExt` adds methods to futures that return `Result` types.
        future::{FutureExt, TryFutureExt},
    },
    std::net::SocketAddr,
};
}

Once the imports are out of the way, we can start putting together the boilerplate to allow us to serve requests:

async fn serve_req(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
    // Always return successfully with a response containing a body with
    // a friendly greeting ;)
    Ok(Response::new(Body::from("hello, world!")))
}

async fn run_server(addr: SocketAddr) {
    println!("Listening on http://{}", addr);

    // Create a server bound on the provided address
    let serve_future = Server::bind(&addr)
        // Serve requests using our `async serve_req` function.
        // `serve` takes a closure which returns a type implementing the
        // `Service` trait. `service_fn` returns a value implementing the
        // `Service` trait, and accepts a closure which goes from request
        // to a future of the response. To use our `serve_req` function with
        // Hyper, we have to box it and put it in a compatability
        // wrapper to go from a futures 0.3 future (the kind returned by
        // `async fn`) to a futures 0.1 future (the kind used by Hyper).
        .serve(|| service_fn(|req| serve_req(req).boxed().compat()));

    // Wait for the server to complete serving or exit with an error.
    // If an error occurred, print it to stderr.
    if let Err(e) = serve_future.compat().await {
        eprintln!("server error: {}", e);
    }
}

fn main() {
    // Set the address to run our socket on.
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    // Call our `run_server` function, which returns a future.
    // As with every `async fn`, for `run_server` to do anything,
    // the returned future needs to be run. Additionally,
    // we need to convert the returned future from a futures 0.3 future into a
    // futures 0.1 future.
    let futures_03_future = run_server(addr);
    let futures_01_future = futures_03_future.unit_error().boxed().compat();

    // Finally, we can run the future to completion using the `run` function
    // provided by Hyper.
    run(futures_01_future);
}

If you cargo run now, you should see the message "Listening on http://127.0.0.1:3000" printed on your terminal. If you open that URL in your browser of choice, you'll see "hello, world!" appear in your browser. Congratulations! You just wrote your first asynchronous webserver in Rust.

You can also inspect the request itself, which contains information such as the request URI, HTTP version, headers, and other metadata. For example, we can print out the URI of the request like this:


#![allow(unused_variables)]
fn main() {
println!("Got request at {:?}", req.uri());
}

You may have noticed that we're not yet doing anything asynchronous when handling the request-- we just respond immediately, so we're not taking advantage of the flexibility that async fn gives us. Rather than just returning a static message, let's try proxying the user's request to another website using Hyper's HTTP client.

We start by parsing out the URL we want to request:


#![allow(unused_variables)]
fn main() {
let url_str = "http://www.rust-lang.org/en-US/";
let url = url_str.parse::<Uri>().expect("failed to parse URL");
}

Then we can create a new hyper::Client and use it to make a GET request, returning the response to the user:


#![allow(unused_variables)]
fn main() {
let res = Client::new().get(url).compat().await;
// Return the result of the request directly to the user
println!("request finished-- returning response");
res
}

Client::get returns a hyper::client::FutureResponse, which implements Future<Output = Result<Response, Error>> (or Future<Item = Response, Error = Error> in futures 0.1 terms). When we .await that future, an HTTP request is sent out, the current task is suspended, and the task is queued to be continued once a response has become available.

Now, if you cargo run and open http://127.0.0.1:3000/foo in your browser, you'll see the Rust homepage, and the following terminal output:

Listening on http://127.0.0.1:3000
Got request at /foo
making request to http://www.rust-lang.org/en-US/
request finished-- returning response

Congratulations! You just proxied an HTTP request.