Skip to content

Instantly share code, notes, and snippets.

@timhughes
Last active March 24, 2024 02:08
Show Gist options
  • Save timhughes/745a07746f96c64586682d78829b840b to your computer and use it in GitHub Desktop.
Save timhughes/745a07746f96c64586682d78829b840b to your computer and use it in GitHub Desktop.
Axum strange routing
[package]
name = "example"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7.4", features = ["http2", "macros"] }
tokio = { version = "1.36.0", features = ["full", "tracing"] }
tower-http = { version = "0.5.2", features = ["trace", "fs", "normalize-path"] }
tower-layer = "0.3.2"
serde_json = "1.0"
http-body-util = "0.1.0"
mime = "0.3"
tracing = "0.1"
[dev-dependencies]
tower = { version = "0.4", features = ["util"] }
use axum::{
extract::Request, http::StatusCode, response::IntoResponse, routing::{get, get_service}, Router, ServiceExt
};
use tokio::net::TcpListener;
use tower_layer::Layer;
use tower_http::{
normalize_path::NormalizePathLayer, services::{ServeDir, ServeFile}, set_status::SetStatus
};
#[axum::debug_handler]
async fn handler_404_a() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "Not Found 'a'")
}
#[axum::debug_handler]
async fn handler_404_b() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "Not Found 'b'")
}
#[axum::debug_handler]
async fn handler_404_root() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "Not Found 'root' - this should never happen")
}
fn b_router() -> Router {
Router::new()
.route("/", get(|| async { "hi I am 'b'" }))
.fallback(handler_404_b)
}
fn a_router() -> Router {
Router::new()
.route("/", get(|| async { "hi I am 'a'" }))
.nest("/b", b_router())
.fallback(handler_404_a)
}
fn app() -> Router {
let serve_dir = get_service(ServeDir::new("static").fallback(SetStatus::new(
ServeFile::new("static/index.html"),
StatusCode::OK,
)));
let app = Router::new()
.nest("/a", a_router())
.route_service("/", serve_dir.clone())
.fallback(handler_404_root);
// .fallback(serve_dir)
app
}
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
let app = app();
let app = ServiceExt::<Request>::into_make_service(
NormalizePathLayer::trim_trailing_slash().layer(app),
);
axum::serve(listener, app).await.unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{body::Body, http::Request};
use http_body_util::BodyExt; // for `collect`
use tower::Service;
use tower::ServiceExt; // for `call`, `oneshot`, and `ready`
#[tokio::test]
async fn test_root() {
let mut app = app().into_service();
let request = Request::builder().uri("/").body(Body::empty()).unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8((&*body).to_vec()).unwrap();
assert_eq!(body_str, "hi I am 'root'");
}
#[tokio::test]
async fn test_root_404_should_return_same_as_root() {
let mut app = app().into_service();
let request = Request::builder()
.uri("/lalala")
.body(Body::empty())
.unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8((&*body).to_vec()).unwrap();
assert_eq!(body_str, "hi I am 'root'");
}
#[tokio::test]
async fn test_a() {
let mut app = app().into_service();
let request = Request::builder().uri("/a").body(Body::empty()).unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8((&*body).to_vec()).unwrap();
assert_eq!(body_str, "hi I am 'a'");
}
#[tokio::test]
async fn test_a_forward_slash() {
let mut app = app().into_service();
let request = Request::builder().uri("/a/").body(Body::empty()).unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let body = response.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8((&*body).to_vec()).unwrap();
assert_eq!(body_str, "Not Found 'a'");
}
#[tokio::test]
async fn test_a_404() {
let mut app = app().into_service();
let request = Request::builder()
.uri("/a/lalala")
.body(Body::empty())
.unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let body = response.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8((&*body).to_vec()).unwrap();
assert_eq!(body_str, "Not Found 'a'");
}
#[tokio::test]
async fn test_b() {
let mut app = app().into_service();
let request = Request::builder().uri("/a/b").body(Body::empty()).unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8((&*body).to_vec()).unwrap();
assert_eq!(body_str, "hi I am 'b'");
}
#[tokio::test]
async fn test_b_forward_slash() {
let mut app = app().into_service();
let request = Request::builder().uri("/a/b/").body(Body::empty()).unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let body = response.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8((&*body).to_vec()).unwrap();
assert_eq!(body_str, "Not Found 'b'");
}
#[tokio::test]
async fn test_b_404() {
let mut app = app().into_service();
let request = Request::builder()
.uri("/a/b/lalala")
.body(Body::empty())
.unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let body = response.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8((&*body).to_vec()).unwrap();
assert_eq!(body_str, "Not Found 'b'");
}
}
@timhughes
Copy link
Author

❯ cargo test -p example 
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running unittests src/main.rs (target/debug/deps/example-2d87fa268c188e6a)

running 8 tests
test tests::test_b ... ok
test tests::test_a_404 ... ok
test tests::test_a ... ok
test tests::test_b_404 ... ok
test tests::test_a_forward_slash ... FAILED
test tests::test_b_forward_slash ... FAILED
test tests::test_root_404_should_return_same_as_root ... FAILED
test tests::test_root ... FAILED

failures:

---- tests::test_a_forward_slash stdout ----
thread 'tests::test_a_forward_slash' panicked at example/src/main.rs:137:9:
assertion `left == right` failed
  left: "Not Found 'root' - this should never happen"
 right: "Not Found 'a'"

---- tests::test_b_forward_slash stdout ----
thread 'tests::test_b_forward_slash' panicked at example/src/main.rs:189:9:
assertion `left == right` failed
  left: "Not Found 'root' - this should never happen"
 right: "Not Found 'b'"

---- tests::test_root_404_should_return_same_as_root stdout ----
thread 'tests::test_root_404_should_return_same_as_root' panicked at example/src/main.rs:100:9:
assertion `left == right` failed
  left: 404
 right: 200
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- tests::test_root stdout ----
thread 'tests::test_root' panicked at example/src/main.rs:84:9:
assertion `left == right` failed
  left: ""
 right: "hi I am 'root'"


failures:
    tests::test_a_forward_slash
    tests::test_b_forward_slash
    tests::test_root
    tests::test_root_404_should_return_same_as_root

test result: FAILED. 4 passed; 4 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `-p example --bin example`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment