I am using Rusts's Axum web framework.
I need to have multiple fallbacks.
First fallback is for delivering static files such as /script.js
and /style.css
located in my static
folder.
Second fallback should be for if any route doesn't match, then I need to deliver the home page (which is also delivered when /
is matched). For example for fake routes which render a client side rendered page.
CODE:
dotenv::dotenv().expect("Failed to load environment variables from .env file!");
let state = AppState {
pool: PgPoolOptions::new().max_connections(70).connect(&std::env::var("DATABASE_URL").expect("DATABASE_URL must be set.")).await.unwrap(),
};
let api_path = "/api";
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
let app = axum::Router::new()
.route("/", get(home_get))
.route_with_tsr(format!("{api_path}/submit").as_str(), post(submit_post))
.route_with_tsr(format!("{api_path}/search").as_str(), post(search_post))
.fallback_service(ServeDir::new(Path::new(&std::env::var("STATIC_FILES").expect("STATIC_FILES must be set."))))
.fallback(get(home_get))
.route_layer(axum::middleware::from_fn_with_state(state.clone(),info_middleware))
.layer(RequestBodyLimitLayer::new(4096))
.layer(TraceLayer::new_for_http())
.with_state(state);
let listener = tokio::net::TcpListener::bind(":::8080").await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();
This isn't working. When going to home page, the static files do not get sent and 500 Internal Error
is sent.
Trace logs:
2025-01-19T04:32:37.437061Z DEBUG request{method=GET uri=/ version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-01-19T04:32:37.437244Z DEBUG request{method=GET uri=/ version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=200
2025-01-19T04:32:37.478539Z DEBUG request{method=GET uri=/style.css?v=1737261157 version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-01-19T04:32:37.480165Z DEBUG request{method=GET uri=/style.css?v=1737261157 version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=1 ms status=500
2025-01-19T04:32:37.480295Z ERROR request{method=GET uri=/style.css?v=1737261157 version=HTTP/1.1}: tower_http::trace::on_failure: response failed classification=Status code: 500 Internal Server Error latency=1 ms
2025-01-19T04:32:37.482660Z DEBUG request{method=GET uri=/script.js?v=1737261157 version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-01-19T04:32:37.487678Z DEBUG request{method=GET uri=/script.js?v=1737261157 version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=5 ms status=500
2025-01-19T04:32:37.487877Z ERROR request{method=GET uri=/script.js?v=1737261157 version=HTTP/1.1}: tower_http::trace::on_failure: response failed classification=Status code: 500 Internal Server Error latency=5 ms
If I comment out the second fallback .fallback(get(home_get))
, then it starts working.
How to have both fallbacks?
A router can only have one fallback route/service. With the .fallback()
call in, this handler replaces the fallback service registered with .fallback_service()
. The 500 is therefore unrelated to the presence of two fallbacks (since this is impossible); instead, the home_get
handler is likely returning this code. You should see identical behavior if you remove the .fallback_service()
call completely. This is a separate bug you'll need to fix.
What you probably want to do instead is use ServeDir::fallback
to chain the home_get
handler as a nested fallback to that service. Note that you need to create a plain tower Service
which has no concept of state, so you need to supply the state again for the MethodRouter
you'll be using as the fallback service (assuming that this handler even needs access to the state; if it does not then you can skip supplying the state).
For example:
.fallback_service(
ServeDir::new(...)
.fallback(get(home_get).with_state(state.clone()))
)