How to host SPA files and embed too with axum and rust-embed

Posted by Abbie Anker on Saturday, May 18, 2024

I'm having hard time understanding how to embed SPA (single page application) files with rust-embed and axum.

I have no trouble without rust-embed using a single line of code with axum (from here):

app.fallback(get_service(ServeDir::new("./app/static")).handle_error(error_handler)) 

It works because all files are correctly downloaded. But:

FIRST PROBLEM

What is missing for a properly SPA handling is the redirect on the index.html if for example the user reloads the page on a SPA nested route.

Example: I'm on the page: /home/customers which is not a file nor a dir but just a fake javascript route and if I reload the page axum gives me 404 (Not found).

SECOND PROBLEM

I need to embed those files in my final executable. In Golang this is "native" using embed: directive.

I saw that in Rust this is well done with rust-embed but I cannot complete my task for SPA.

The need is that every path typed by the user (and that is not an existent file such as .js or .css which obviously must be downloaded by the browser) leads to the "index.html" file in the root of my static dir.

If I use the example axum code I can see the route:

.route("/dist/*file", static_handler.into_service()) 

which has /dist/*file and I don't need that /dist because the index.html calls many files with custom paths, such as /_works, menu, images.

If I remove the dist part I get this error:

thread 'main' panicked at 'Invalid route: insertion failed due to conflict with previously registered route: /index.html' 

Can you help me understand how to properly accomplish this task?

Thanks.

1

1 Answer

I had a similar issue, building with Vue and Axum/Rust. Here's how I solved Problem one

  • Install the tower_http crate
  • use axum::routing::get_service to serve the build SPA.
  • //example implementation ... //static file mounting let assets_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("views"); let static_files_service = get_service( ServeDir::new(assets_dir).append_index_html_on_directories(true), ) .handle_error(|error: std::io::Error| async move { ( StatusCode::INTERNAL_SERVER_ERROR, format!("Unhandled internal error: {}", error), ) }); ... 
  • Mount the static file rendering
  •  //mount the app routes and middleware let app = Router::new() .fallback(static_files_service) .nest("/api/v1/", routes::root::router()) .layer(cors) .layer(TraceLayer::new_for_http()) .layer(Extension(database)); 

    Check out the full source code here. Another thing is, Axum seems to have breaking changes in subsequent versions as I found out here, so you might need to check the doc/example that corresponds to the version of Axum you are using :)

    ncG1vNJzZmirpJawrLvVnqmfpJ%2Bse6S7zGiorp2jqbawutJobmxsZmmBeIWOoaawZaSkeqm70q1krKiRYrOquMSsZJqmlGKyrq7EnWStp59ixKrAx2aYsa2dYq6vsIyrrKysXZq6o7HD