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.
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:
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:
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:
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:
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:
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), )), } }