Search code examples
rustrust-axum

multiple fallbacks causes them to fail with 500 Internal Error


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?


Solution

  • 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()))
    )