Extractors

Axum provides extractors to get/extract data about requests to our handlers. To qualify as a handler, parameters of a function must implement FromRequestParts last parameter can implement FromRequest (meaning it also implements FromRequestParts). There are two traits because some parts of request for example body can only be extracted once (can be cloned later).

In previous hello world example we didn't need any info about requests for our app to run. I was thinking, "Okay, I need to build some random app to explain readers how extractors work." What did I do? Of course I found something by randomly traveling in axum docs.

Every developer needs a way to get their clients ip address and sell it to big corporations. But of course as ethical developers, we will not do that. Instead we will explain the client we got their ip address and they can possibly be identified by harmful parties and to prevent that they need to buy our VPN service.

Here is a basic example of our app:

use axum::{extract::ConnectInfo, routing::get, Router};
use std::net::SocketAddr;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let app = Router::new().route("/", get(index));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;

    axum::serve(
        listener,
        app.into_make_service_with_connect_info::<SocketAddr>(),
    )
    .await?;

    Ok(())
}

async fn index(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
    format!(
        "Your ip address is \"{addr}\".\n\
         You are in immediate danger of getting identified by bad people.\n\
         Thankfully we have a VPN service to hide your ip. \n\
         Visit this link to download it \"http://localhost:3000/average_joe_absolutely_needs_vpn\""
    )
}

To be able to get ip address with ConnectInfo extractor we need this boilerplate code:

#![allow(unused)]
fn main() {
// copypasta for future projects
axum::serve(
    listener,
    app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
}

Lets take a look at function signature:

#![allow(unused)]
fn main() {
async fn index(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String
}

You might not be familiar of pattern matching done for function parameter "addr" here. Lets explain shortly.

ConnectInfo is a struct. It is defined something like struct ConnectInfo<T>(T);. It has paranthesis instead of braces and can take any type like a tuple with single item. (example let something = ConnectInfo(String);) By using tuple structs in function parameters you can match inner item. If we used addr: ConnectInfo<SocketAddr> instead we would have to do let addr = addr.0; in the code below to achieve the same thing.

To simplify, for now lets talk about it using the other syntax.

#![allow(unused)]
fn main() {
async fn index(addr: ConnectInfo<SocketAddr>) -> String
}

addr parameter type needs to implement FromRequest or FromRequestParts for this function to qualify as a handler. ConnectInfo implements it if we use it like how it is documented.

We are dynamically creating a string to add ip address inside. So we need to use String type which implements IntoResponse.

In the handler code, its basically just format! macro to create a string so nothing new there.

Our app works fine but its just plaintext. Lets improve it by doing some html.

#![allow(unused)]
fn main() {
// change return type to `Html<String>` to let browser know we are sending html
async fn index(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> Html<String> {
    let html = format!(
        "<h1>Your ip address is: \"{addr}\"</h1>\n\
         <h2>You are in immediate danger of getting identified by bad people.</h2>\n\
         <h2>Thankfully we have a VPN service to hide your ip.</h2>\n\
         <h2>Visit <a href=\"http://localhost:3000/average_joe_absolutely_needs_vpn\">THIS</a> link to download it.</h2>"
    );

    // create `Html` type like this
    Html(html)
}
}

Challenge

Create a /average_joe_absolutely_needs_vpn page returning html to convince the poor guy client to buy our VPN service.