Axum cannot serve svelte files

Axum cannot serve svelte files

^ this seems to the crux of the issue, when I serve statically built svelte files, and some static assets, when svelte tries to call internal resources, it is either not delivered correctly, or blocked, for reasons I do not know

Full code (a small portion is relevent):

(says blocked due to mime type? also just not found)

I think this is not a rust issue and therefore not relevant here, I thought the issue was how I do mime types or how i serve it on the backend, the issue is, is that I serve all my routes behind a prefix (gameserver-rs) for reasons, and back when i just used html, I was able to replace a varible which set a base path, however I dont know the equivalent in svelte, this has to be dynamic. So I doubt this can be solved here unless you know svelte or a solution for the backend.

Might be about Vite builds?

Or the UTF-8 character set?

Your example does a ton. Try making a SSCE, might help isolate the issue https://sscce.org/

Maybe this works? Can you serve an “empty” svelte app like this?

use axum::Router;
use tower_http::services::ServeDir;

let static_service = ServeDir::new("dist");
let app = Router::new()
    .fallback_service(static_service);

I have a project you can try:

but it might be the easiest thing to expect of someone, (plus you'll have to run src/gameserver/ which is also a cargo project, simultaneously, as thats required)

I'll show you my relevant snippets of code:


    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods([Method::GET, Method::POST])
        .allow_headers(Any);

        let fallback_router = routes_static(state.clone().into());

        let inner = Router::new()
            .route("/api/message", get(get_message))
            .route("/api/nodes", get(get_nodes))
            .route("/api/ws", get(ws_handler))
            .route("/api/users", get(users))
            .route("/api/send", post(receive_message))
            .route("/api/general", post(process_general))
            .route("/api/signin", post(sign_in))
            .route("/api/createuser", post(create_user))
            .route("/api/deleteuser", post(delete_user))
            .merge(fallback_router)
            .with_state(state.clone());
        

    let app = if base_path.is_empty() || base_path == "/" {
        inner.layer(cors)
    } else {
        Router::new().nest(&base_path, inner).layer(cors)
    };


    let addr: SocketAddr = LocalUrl.parse().unwrap();
    println!("Listening on http://{}{}", addr, base_path);

    // let addr: SocketAddr = "0.0.0.0:8081".parse().unwrap();
    // println!("Listening on http://{}{}", addr, base_path);
    // axum::serve(TcpListener::bind(addr).await?, app).await?;

    // Updated server start:
    let listener = TcpListener::bind(addr).await?;
    axum::serve(listener, app.into_make_service())
        .await?;

    let base_path = std::env::var("SITE_URL")
        .map(|s| {
            let mut s = s.trim().to_string();
            if !s.is_empty() {
                if !s.starts_with('/') { s.insert(0, '/'); }
                if s.ends_with('/') && s != "/" { s.pop(); }
            }
            s
        })
        .unwrap_or_default();

^ this is just the main function, the rest is:

async fn serve_html_with_replacement(
    file: &str,
    state: &AppState,
) -> Result<Response<Body>, StatusCode> {
    let path = Path::new("src/svelte/dist").join(file);

    if path.extension().and_then(|e| e.to_str()) == Some("html") {
        let html = tokio_fs::read_to_string(&path)
            .await
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
        let replaced = html.replace("[[SITE_URL]]", &state.base_path);
        return Ok(Html(replaced).into_response());
    }

    let bytes = tokio_fs::read(&path)
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;
    let content_type = from_path(&path).first_or_octet_stream().to_string();

    Ok(Response::builder()
        .header("Content-Type", content_type)
        .body(Body::from(bytes))
        .unwrap())
}

async fn handle_static_request(
    Extension(state): Extension<Arc<AppState>>,
    req: Request<Body>,
) -> Result<Response<Body>, StatusCode> {
    let mut path = req.uri().path().to_string();

    let base_path = &state.base_path;

    // if let Some(stripped) = path.strip_prefix(base_path) {
    //     path = stripped.to_string();
    // }

    let file = if path == "/" || path.is_empty() {
        "index.html".to_string()
    } else {
        path.trim_start_matches('/').to_string()
    };

    match serve_html_with_replacement(&file, &state).await {
        Ok(res) => Ok(res),
        Err(status) => Ok(Response::builder()
            .status(status)
            .header("content-type", "text/plain")
            .body(format!("Error serving `{}`", file).into())
            .unwrap()),
    }
}

Not that short, but I hope easy to read, now the main issue I am having is, again, I dont know how to dynamically tell svelte where the base path is, i explained how i substituted it with html, but svelte bundles their own stuff, which you could control over some statically set stuff, but thats not what I want, I was hoping I could fix this in the backend, in my main function you can see how i nest everything under the base path, so everything will be there, so when svelte asks for a asset at http://localhost:8080/_app there is a immediate issue, in the backend, I am wondering how I could give svelte access to its assets by re-routing the requests, (preferably not making the entire site accessible without the gameserver prefix but at this point ill take anything). Or some substitution method, I was suggested two things:

alternatively create a [...rest] endpoint in sveltekit and proxy the other routes to your backend

Issue with 1, ill need to run another process simultaneously, issue with 2, I dont think itll work for internal assets like under _app? I tried to keep this purely rust based, and while the solution could be in rust, i might need help will how I approach this whole thinf

Could it be that you used lower case on the Content-Type header name?

I would change the framing: Svelte assumes things that Axum can't provide.

Lemme back up. I've written and am still working on a system that uses Axum and SvelteKit together, successfully. I can understand why you're having trouble: both pieces of software are very opinionated about their respective domains, and those opinions are not always aligned with each other.

For example, SvelteKit wants developers to not worry about whether code runs on the server or on the client, on the premise that that decision can happen at deployment time, transparently (so long as client and server communicate in the ways SvelteKit supports). However, that assumption can only be true if the server can run SvelteKit - which effectively means "if the server is written in Node." Axum, obviously, isn't.

The approach I settled on, which does work quite well, uses SvelteKit's "SPA" mode to force pre-rendering of the entire SvelteKit application to a set of static files, and disables SvelteKit's support for server-side anything. That's done in SvelteKit in a few places:

  • The rootmost +layout.js exports a flag to disable server-side rendering:

    export const ssr = false;
    
  • svelte.config.js is set up to use the static adapter explicitly, with an explicit pages configuration setting controlling where vite build writes the resulting static files.

  • My Axum application serves exactly that output directory, not the default svelte-kit output directory, to clients.

You can see the whole project here. Axum glue is under src/ui. Unlike your application, mine does not allow the user to change the URL path; I haven't tried to make that work as I had concerns it'd be a pain in the ass.


I love Axum to bits and I've been enjoying using Svelte, but my recommendation would be to either use Axum with something simpler for generating your client application, or use Svelte with a node-based server, unless you are comfortable writing a fair bit of glue. Maybe I'll open-source the stuff I've written to make this work, or someone else will write some integration that's solid-enough for public consumption, but as things stand, using those two things together requires an inordinate amount of familiarity with both languages, plus familiarity with both HTTP and the browser ecosystem. It's a challenge, and your struggle with it is unfortunately a very normal one.

4 Likes