A painting of a woman in a seaside cafe on a glorious summer dayA woman in a cafe on a beautiful day

Intro

Perhaps you'd like to run a fileserver with Axum, or maybe just serve static content in a folder. It's a bit trickier in Axum than you might expect.

Background

I was recently converting an example for webauthn-rs from tide to axum, because I really liked that it uses the types of the http crate and interoperates easily with tower layers. But I hit a snag.

The server runs a Webassembly client library in a server rendered page, which means it needs to provide a .wasm binary file and the JS glue code to run it.

For those curious, this is the function that does that. It returns an HTML document with a <script> tag that asynchronously loads the web assembly binary file.

Rust code
async fn index_view(_request: tide::Request<AppState>) -> tide::Result {
    let mut res = tide::Response::new(200);
    res.set_content_type("text/html;charset=utf-8");
    res.set_body(
        r#"
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>WebAuthn-rs Tutorial</title>
    <script type="module">
        import init, { run_app } from './pkg/wasm.js';
        async function main() {
           await init('./pkg/wasm_bg.wasm');
           run_app();
        }
        main()
    </script>
  </head>
  <body>
  </body>
</html>
    "#,
    );
    Ok(res)
}

For more details on how to load Webassembly files in Rust, checkout this detailed blog post from Tung's Word Box.

In tide, the route responsible for serving this file looks like this:

Rust code
    let mut app = tide::with_state(app_state);
		...
    // Serve our wasm content
    app.at("/pkg").serve_dir("../../wasm/pkg")?;
		...

You might think, as I have, that there would be an equivalent of .serve_dir() in Axum, but you would be mistaken.

Serve Files with Axum

Since there's no easy handler, we've got a bit of work to do. First, we need to define a function that will get the file from the filesystem , and returns a response with the file contents in byte format. Thankfully, the tower ecosystem has a handy helper in tower_http called ServeDir. Just add it to your cargo.toml, and don't forget the fs feature flag! Thus, we can build the following function to create that response:

Rust code
async fn get_static_file(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
    let req = Request::builder().uri(uri).body(Body::empty()).unwrap();

    // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
    // When run normally, the root is the workspace root
    match ServeDir::new("../../wasm/pkg").oneshot(req).await {
        Ok(res) => Ok(res.map(boxed)),
        Err(err) => Err((
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Something went wrong: {}", err),
        )),
    }
}

You can see that we are distributing files from the filepath indicated in the new() function, and that it is relative to the root where you run your app.

Now that we have that, we can write a handler to take in a URI:

Rust code
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
    let res = get_static_file(uri.clone()).await?;

    if res.status() == StatusCode::NOT_FOUND {
        // try with `.html`
        // TODO: handle if the Uri has query parameters
        match format!("{}.html", uri).parse() {
            Ok(uri_html) => get_static_file(uri_html).await,
            Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
        }
    } else {
        Ok(res)
    }
}

Now we just need to add a route in our Axum router like so:

Rust code
 let app = Router::new()
     .nest("/pkg", get(file_handler));

.nest() collects all the file paths off of the parent, making it perfect for distributing a particular file.And that's it, 24 lines of code plus an external tower service to match Tide's .serve_dir() helper. But hey, it works just fine! And ServeDir is neat because you can call it inside of any other handler.

Thanks

Big thank you to @davidpdrsn on github for posting his version in this github discussion, which only needed some upgrades for various changes in types.

Full Code:

Rust code
use axum::{
    body::{boxed, Body, BoxBody},
    http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;

pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
    let res = get_static_file(uri.clone()).await?;
    println!("{:?}", res);

    if res.status() == StatusCode::NOT_FOUND {
        // try with `.html`
        // TODO: handle if the Uri has query parameters
        match format!("{}.html", uri).parse() {
            Ok(uri_html) => get_static_file(uri_html).await,
            Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
        }
    } else {
        Ok(res)
    }
}

async fn get_static_file(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
    let req = Request::builder().uri(uri).body(Body::empty()).unwrap();

    // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
    // When run normally, the root is the workspace root
    match ServeDir::new("./webauthn_client/pkg").oneshot(req).await {
        Ok(res) => Ok(res.map(boxed)),
        Err(err) => Err((
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Something went wrong: {}", err),
        )),
    }
}