Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make routing setup for SPAs easier #87

Closed
davidpdrsn opened this issue Aug 2, 2021 · 11 comments
Closed

Make routing setup for SPAs easier #87

davidpdrsn opened this issue Aug 2, 2021 · 11 comments
Labels
C-musings Category: musings about a better world

Comments

@davidpdrsn
Copy link
Member

For SPAs its often useful to first check if the URI matches some static resource (like javascript or css) and if not fallback to calling a handler. The handler should receive the entire URI, regardless of what it is, so routing can be done client side.

Its might be possible to do such a setup with axum today using some combination of ServeDir (from tower-http) and nest but I wouldn't be very ergonomic. We should investigate ways to make it easier.

I'm not exactly sure what the best and most general solution is. Could be wildcard routes similarly to what tide has. I think looking into how tide handles setting up something like this and then learning from that is a good place to start.

@thedodd
Copy link

thedodd commented Aug 2, 2021

I am hoping to cut https://github.com/thedodd/trunk over to axum, but I have a use case where users may define arbitrary proxies for their applications, and the current routing paradigm does not seem to support the possibility of matching any request "below" a specific prefix.

EG, if a user defines a proxy endpoint at /api/v2/, I need to be able to accept any request matching that prefix, and then proxy that request to the configured backend. Hopefully that makes sense. Let me know if there is already a way to do this. If there is, I can't seem to derive how to do so from the current docs.

@programatik29
Copy link
Contributor

@thedodd I think nest should work.

@thedodd
Copy link

thedodd commented Aug 2, 2021

@programatik29 looks like that might be perfect! Thanks for the quick response.

@davidpdrsn
Copy link
Member Author

A possible routing setup for SPAs could be:

use axum::{prelude::*, routing::nest, service::ServiceExt};
use http::StatusCode;
use std::{convert::Infallible, net::SocketAddr};
use tower_http::services::{ServeDir, ServeFile};

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let app = nest(
        "/",
        axum::service::get(ServeFile::new("assets/index.html")),
    )
    .nest(
        "/assets",
        axum::service::get(ServeDir::new("assets")),
    )
    .handle_error(|err| {
        Ok::<_, Infallible>((
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Unhandled error: {}", err),
        ))
    })
    .route("/ws", axum::ws::ws(handle_socket));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::debug!("listening on {}", addr);
    hyper::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn handle_socket(socket: axum::ws::WebSocket) {
    // ...
}

main difference being that assets are served at /assets and not /.

@programatik29
Copy link
Contributor

Another possible setup:

use std::net::SocketAddr;

use axum::routing::nest;
use axum::routing::RoutingDsl;

use tower::{service_fn, BoxError, Service};
use tower_http::services::{ServeDir, ServeFile};

use http_body::combinators::BoxBody;

#[tokio::main]
async fn main() {
    let app = nest(
        "/",
        axum::service::get(service_fn(|req| async {
            let resp = ServeDir::new("html").call(req).await?;
            let resp = resp.map(|body| BoxBody::new(body));

            if resp.status() == 404 {
                let resp = ServeFile::new("html/index.html").call(()).await?;
                let resp = resp.map(|body| BoxBody::new(body));

                return Ok(resp);
            }

            Ok::<_, BoxError>(resp)
        })),
    );

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    hyper::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Creative usage of axum. Not ergonomic but does the job.

@davidpdrsn davidpdrsn added the C-musings Category: musings about a better world label Aug 3, 2021
@davidpdrsn
Copy link
Member Author

Another approach, using or from the upcoming version 0.2:

use axum::{handler::get, http::StatusCode, response::IntoResponse, service, Router};
use std::{convert::Infallible, io, net::SocketAddr};
use tower_http::services::{ServeDir, ServeFile};

#[tokio::main]
async fn main() {
    // our API routes
    let api_routes = Router::new().route("/users", get(|| async { "users#index" }));

    let app = Router::new()
        // serve static files at `GET /assets/*`
        .nest(
            "/assets",
            service::get(ServeDir::new("assets")).handle_error(handle_io_error),
        )
        // serve the API at `/api/*`
        .nest("/api", api_routes)
        // all other requests will receive `index.html` regardless of HTTP method or URI
        .or(service::any(ServeFile::new("index.html")).handle_error(handle_io_error));

    // run
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

fn handle_io_error(error: io::Error) -> Result<impl IntoResponse, Infallible> {
    Ok((
        StatusCode::INTERNAL_SERVER_ERROR,
        format!("Unhandled error: {}", error),
    ))
}

@davidpdrsn
Copy link
Member Author

I think I'll close this for now. I think the examples shared here are good starting points. If someone has specific questions feel free to ask here.

@Stefan99353
Copy link

Stefan99353 commented Mar 16, 2022

I am trying to somewhat combine the last two approaches. I want to serve my SPA files at the root and have API routes. I tried serving my SPA files in /static but my URL in the browser changes to localhost/staticfor my index.html (On reload the browser would request /static/index.html which does not exist).

Basically I want the following:

Router::new()
    // Serve API
    .nest("/api", get_api_router())
    // Serve SPA
    .nest(
        "/",
        get_service(
            ServeDir::new("./static")
        )
        .handle_error(|_: std::io::Error| async move {
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(json!({"error": "Internal Server Error"})),
            )
        }),
    )
    // Unknown routes to index.html for client side routing
    .fallback(
        get_service(
            ServeFile::new("./static/index.html")
        )
        .handle_error(|_: std::io::Error| async move {
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(json!({"error": "Internal Server Error"})),
            )
        }),
    );

This does not work because .nest("/", _) conflicts with my API routes: Note that 'nest("/", _)' conflicts with all routes.

In actix-web I was able to do this:

App::new()
    .service(get_api_scope())
    .service(Files::new("/", "./static").index_file("index.html"))
    .default_service(|r: ServiceRequest| futures::future::ok(spa_index(r)));

fn spa_index(service_request: ServiceRequest) -> ServiceResponse {
    let (req, _payload) = service_request.into_parts();
    let file_response = NamedFile::open("./static/index.html").respond_to(&req);
    ServiceResponse::new(req, file_response.map_into_boxed_body())
}

Can someone point me in the right direction? Thanks!

@davidpdrsn
Copy link
Member Author

@edlingao
Copy link

@davidpdrsn I think that link is broken

@davidpdrsn
Copy link
Member Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-musings Category: musings about a better world
Projects
None yet
Development

No branches or pull requests

5 participants