Axum By Example (Static File Server)


Axum By Example (Static File Server)
🦀 One of the best ways to understand code is to read more of it.
I would like to take a second to welcome you to this article. I hope you're having a great day. Be safe and have fun!
Static File Server - axum examples
It seems that there is something wrong with this example. When I run the server
and start the project. When there is an attempt to access localhost:3001
nothing shows up. So we will have to work with an idea. We will just start going
through the code to understand it. That's the only way that we can figure it
out. In this example, it is going to be simplified to one route. So we can debug a smaller project before we build it back out.
🛑 The problem was where I was starting the program. I was starting it in the
axum
directory. When I needed to be in theaxum/example/static-file-server/
to allow for the relative path to be in the correct location. It works fine. The problem was me 👀
//! Run with
//!
//! ```not_rust
//! cd axum/examples/static-file-server
//! cargo run
//! ```
use axum::{
extract::Request, handler::HandlerWithoutStateExt, http::StatusCode, routing::get, Router,
};
use std::net::SocketAddr;
use tower::ServiceExt;
use tower_http::{
services::{ServeDir, ServeFile},
trace::TraceLayer,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "example_static_file_server=debug,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
tokio::join!(
serve(using_serve_dir(), 3001),
);
}
fn using_serve_dir() -> Router {
// serve the file in the "assets" directory under `/assets`
Router::new().nest_service("/assets", ServeDir::new("assets"))
}
async fn serve(app: Router, port: u16) {
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app.layer(TraceLayer::new_for_http()))
.await
.unwrap();
}
Breaking down the protect
- imports
- main function
- directory service path
- serve function
Imports
axum
: extractors, handlers, statusCodes, get, and RouterSocketAddr
fromstd::net
tower
tower_http
: serving directory, and serving file-
tracing_subscriber
: Part of a largertracing
ecosystem. It provides a framework for structured, composable logging and diagnostics in Rust applications. It helps configure and set up tracing instrumentation in Rust projects. It provides various subscriber implementations, which are responsible for receiving tracing events and processing them in different ways. Printing to the console, writing to a file, or sending to a distributed tracing system. We can customize the logging behavior and choose which subscribers to use in the application.
use axum::{
extract::Request, handler::HandlerWithoutStateExt, http::StatusCode, routing::get, Router,
};
use std::net::SocketAddr;
use tower::ServiceExt;
use tower_http::{
services::{ServeDir, ServeFile},
trace::TraceLayer,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
Main function
#[tokio::main]
- tracing setup
tokio::join!
the paths together- calling out
serve
function
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "example_static_file_server=debug,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
tokio::join!(
serve(using_serve_dir(), 3001),
);
}
Setting up basic serve directory
- Router pointing to the assets directory
-
Router::new().nest_service
where we connect the/assets
directory to theServeDir::new("assets")
for a simple example.
fn using_serve_dir() -> Router {
// serve the file in the "assets" directory under `/assets`
Router::new().nest_service("/assets", ServeDir::new("assets"))
}
Setting up our serve function
- async
- pulling out the
addr
usingSocketAddr
and the inputport
- Print where we are listening
- Attach the layer to the server.
-
TraceLayer::new_for_http
creates a newTraceLayer
usingServerErrorAsFailures
which supports classifying regular HTTP request based on the status code.
async fn serve(app: Router, port: u16) {
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app.layer(TraceLayer::new_for_http()))
.await
.unwrap();
}
Running the server and checking it out
You must be in the directory where /assets
lives. So the folder where you can
access static-file-server/src/main.rs
is a good place if you're using the
example. Or just in the relative directory as the /assets
.
We can start the program with cargo run
. Then run some sure commands to grab
the files we have in the /assets
directory.
Curl Request Examples
- curl
/assets/index.html
❯ curl http://localhost:3001/assets/index.html
Hi from index.html
- curl
/assets/script.js
❯ curl http://localhost:3001/assets/script.js
console.log("Hello, World!");
This is a good start. There are more examples in this we will now go over. We just wanted to focus on what makes this work at the bare minimum.
Let's add the new routes.
#[tokio::main]
async fn main() {
// same as before we are just expanding the `tokio::join!`
tokio::join!(
serve(using_serve_dir(), 3001),
serve(using_serve_dir_with_assets_fallback(), 3002),
serve(using_serve_dir_only_from_root_via_fallback(), 3003),
serve(using_serve_dir_with_handler_as_service(), 3004),
serve(two_serve_dirs(), 3005),
serve(calling_serve_dir_fron_a_handler(), 3006),
);
}
Using serve dir with assets fallback
We are going to use a fallback of this next service. If we don't choose a path
to traverse. We will go to assets/index.html
.
- Setup the
serve_dir
withServeDir::new("assets")
- Add on a
not_found_service()
withServerFile::new("assets/index.html")
- Add a new route called
/foo
that returnsHi from /foo
- We nest the service with
/assets
and ourserve_dir
-
We add out
.fallback_service
with theserve_dir
which will handle our not found case
fn using_serve_dir_with_assets_fallback() -> Router {
// `ServeDir` allows setting a fallback if an asset is not found
// so with this `GET /assets/doesnt-exist.jpg` will return `index.html`
// rather than a 404
let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html"));
Router::new()
.route("/foo", get(|| async { "Hi from /foo" }))
.nest_service("/assets", serve_dir.clone())
.fallback_service(serve_dir)
}
Curl Request Examples
We will test this one as well with some curl requests.
- curl
/foo
❯ curl http://localhost:3002/foo
Hi from /foo
- curl the port
3002
❯ curl http://localhost:3002
Hi from index.html
- curl
/assets/script.js
❯ curl http://localhost:3002/assets/script.js
console.log("Hello, World!");
It's a very simple design for a simple file server, but is it enough? I don't think so. We have a few more route styles to check out.
Using serve dir only from root via fallback
What are we changing on this path compared to the last path. We are avoiding the
nest_service
section where we attach the serve_dir
to the path. We only
include in in the fallback_service
. Which still allows us to access these
files. It just does not keep the files behind the localhost:3003/assets/script.js
anymore. It goes to localhost:3003/script.js
.
- The fallback is still
index.html
/foo
path works/script.js
works/index.html
works
fn using_serve_dir_only_from_root_via_fallback() -> Router {
// you can also serve the assets directly from the root (not nested under `/assets`)
// by only setting a `ServeDir` as the fallback
let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html"));
Router::new()
.route("/foo", get(|| async { "Hi from /foo" }))
.fallback_service(serve_dir)
}
Curl Request Examples
Let's make some curl requests to show what's actually going on.
- curl the port
3003
❯ curl http://localhost:3003
Hi from index.html
- curl
/foo
❯ curl http://localhost:3003/foo
Hi from /foo
- curl
/assets
❯ curl http://localhost:3003/assets/
Hi from index.html
- curl
/assets/script.js
❯ curl http://localhost:3003/assets/script.js
Hi from index.html
- curl
/script.js
❯ curl http://localhost:3003/script.js
console.log("Hello, World!");
Using serve dir with handler as service
- Create the
handle_404
handler - Convert the
handle_404
into a service usinginto_service()
- Create a
serve_dir
that set's up theassets
path - Attach a
not_found_service()
with the converted handlerservice
The assets will work on the root path and anything not found will return the created handler.
fn using_serve_dir_with_handler_as_service() -> Router {
async fn handle_404() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "Not found")
}
// you can convert handler function to service
let service = handle_404.into_service();
let serve_dir = ServeDir::new("assets").not_found_service(service);
Router::new()
.route("/foo", get(|| async { "Hi from /foo" }))
.fallback_service(serve_dir)
}
Curl Request Examples
- curl port
3004
❯ curl http://localhost:3004
Hi from index.html
- curl
/foo
❯ curl http://localhost:3004/foo
Hi from /foo
- curl
/assets/script.js
❯ curl http://localhost:3004/assets/script.js
Not found
- curl
/script.js
❯ curl http://localhost:3004/script.js
console.log("Hello, World!");
- curl
/assets/index.html
❯ curl http://localhost:3004/assets/index.html
Not found
Two serve dirs
- Create
serve_dir_from_assets
to point to the assets too - Create
serve_dir_from_dist
to point to the dist too
There is a problem here. Can you catch it before I tell you? It's not super easy to catch because it involved to file system that we are using.
- What is the
dist
relative path? - What is
/dist
connecting to?
It seems like there is nothing here for dist. We don't have the directory so we
cannot really connect /dist
to nothing. It's just going to respond with a 404.
We should add a /dist
into our Rust🦀 example. We will also add a dist.html
file inside that returns. Hello from /dist/dist.html
fn two_serve_dirs() -> Router {
// you can also have two `ServeDir`s nested at different paths
let serve_dir_from_assets = ServeDir::new("assets");
let serve_dir_from_dist = ServeDir::new("dist");
Router::new()
.nest_service("/assets", serve_dir_from_assets)
.nest_service("/dist", serve_dir_from_dist)
}
Curl Request Examples
Let's do those curls
- curl
/dist/dist.html
❯ curl http://localhost:3005/dist/dist.html
Hello from /dist/dist.html
- curl
/assets/script.js
❯ curl http://localhost:3005/assets/script.js
console.log("Hello, World!");
- curl
/assets/index.html
❯ curl http://localhost:3005/assets/index.html
Hi from index.html
- curl
/assets
: this works because it's looking for an index.html to return
❯ curl http://localhost:3005/assets
Hi from index.html
- curl
/dist
: this does not work because the html is not the index.html
❯ curl http://localhost:3005/dist
If we make a new file inside of the /dist
directory called index.html
- curl
/dist
again
❯ curl http://localhost:3005/dist
Hello from dist.html
Calling serve dir from a handler
-
I'm not sure what
#[allow(clippy::let_and_return)]
does. I'm not sure if it is necessary. The program seems to run with or without it. - Consume the service with
oneshot
with the request as the input value
Not completely sure about these different use cases. It seems as if it's the
same type of setup. Except you are creating a service and returning inside of
the /foo
. We could get similar results for the functions previously. Although
maybe we want to be able to do something inside of the nest_service
get
request.
#[allow(clippy::let_and_return)]
fn calling_serve_dir_from_a_handler() -> Router {
// via `tower::Service::call`, or more conveniently `tower::ServiceExt::oneshot you can
// call `ServeDir` yourself from a handler
Router::new().nest_service(
"/foo",
get(|request: Request| async {
let service = ServeDir::new("assets");
let result = service.oneshot(request).await;
result
}),
)
}
Curl Request Examples
- curl our port
3006
and nothing returns
❯ curl http://localhost:3006
- curl
/foo
❯ curl http://localhost:3006/foo
Hi from index.html
- curl
/foo/script.js
❯ curl http://localhost:3006/foo/script.js
console.log("Hello, World!");
- curl
/foo/index.html
which returns the same as/foo
❯ curl http://localhost:3006/foo/index.html
Hi from index.html
- curl the non-existing
/assets/index.html
❯ curl http://localhost:3006/assets/index.html
Conclusion 🦀
I enjoy axum
. I do find that it's simple in the design especially in creating
a simple file server. This is something I plan to use more of. May even setup
something to run locally on a spare raspberry pi for testing purposes.
If we are to compare it to JavaScript's express. I would expect something as more performant. Hopefully around 5-8x, but without testing this is just educated guess based on other data points I've seen. The only way I would be able to prove this. Would be to setup these servers and really test them out.