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

Adding nested fallbacks is harder than it should be #1053

Closed
davidpdrsn opened this issue May 25, 2022 · 9 comments
Closed

Adding nested fallbacks is harder than it should be #1053

davidpdrsn opened this issue May 25, 2022 · 9 comments
Assignees
Labels
A-axum C-feature-request Category: A feature request, i.e: not implemented / a PR. E-hard Call for participation: Experience needed to fix: Hard / a lot S-blocked Status: marked as blocked ❌ on something else such as a PR or other implementation work.
Milestone

Comments

@davidpdrsn
Copy link
Member

We currently don't support nesting routers that have fallbacks. This panics:

let api_routes = Router::new()
    .route("/v1/foo", get(|| async {}))
    .fallback((|| async { "api fallback" }).into_service());

let app = Router::new()
    .nest("/api", api_routes)
    .fallback((|| async { "general fallback" }).into_service());

This is because while Router::nest takes T: Service it attempts to downcast that into a Router. If that succeeds it takes a different code path that simply moves routes from one router to another while adding the prefix. However that means we cannot have two fallbacks since we only have one Router in the end.

A possible workaround is to use another Router as a fallback:

let api_routes = Router::new()
    .route("/v1/foo", get(|| async {}));

let app = Router::new()
    .nest("/api", api_routes)
    .fallback(
        Router::new()
            .nest("/api", (|| async { "api fallback" }).into_service())
            .fallback((|| async { "general fallback" }).into_service()),
    );

While this works its not very obvious and it would be great if multiple nested fallbacks just worked.

Haven't thought through how to make it work but I believe it requires ibraheemdev/matchit#18

@davidpdrsn davidpdrsn added C-feature-request Category: A feature request, i.e: not implemented / a PR. A-axum labels May 25, 2022
@lilyball
Copy link

In the short term, I believe that simply treating the nested router as an opaque service in the case where it has a fallback would work. You cannot currently add a nested router that has a fallback, so that approach can't possibly break any code. This would only need to be done if the nested router actually has a fallback, otherwise the current behavior is fine.

@davidpdrsn
Copy link
Member Author

That's a good idea actually 🤔 might get us most of the way there.

@davidpdrsn
Copy link
Member Author

I've thought about this some more and ran into this:

let api = Router::new()
    .fallback(api_fallback);

let app = Router::new()
    .nest("/api", api)
    .fallback(general_fallback)
    .route_layer(some_layer);

Middleware added with route_layer should not be called for fallbacks. But if nest detects that the router has a fallback and instead automatically nests it as an opaque service then the middleware will be called for api_fallback.

I think thats very surprising so unless we can find a solution for that I don't think we should do it.


The solution I have in mind is once ibraheemdev/matchit#18 is fixed then we change fallbacks to be regular wildcard routes. It would require tracking which routes were added with Router::fallback (since they're all just stored together in map currently) such that route_layer can skip those.

Then nesting routers will fallbacks should just work since the nested fallback would become /nested-path/*fallback.

@lilyball
Copy link

I don't know what the expected time frame for ibraheemdev/matchit#18 is. If it will be a while, another potential workaround would be to detect the nested router w/fallback, don't merge it, but do convert all of its own routes to be wrapped in StripPrefix (instead of wrapping the whole router with that). Then route_layer itself could detect that the service for the nested route is a Router and call route_layer on that nested Router instead of applying the layer around it (like how it does for Endpoint::MethodRouter).

If I correctly understand the way this works, that would have a similar effect to merging the nested router with ibraheemdev/matchit#18 fixed, and since it's pushing the StripPrefix out to the nested routes that would preserve the full path for route layers.

@davidpdrsn davidpdrsn added this to the 0.6 milestone Jun 25, 2022
@davidpdrsn davidpdrsn added the S-blocked Status: marked as blocked ❌ on something else such as a PR or other implementation work. label Jul 11, 2022
@davidpdrsn
Copy link
Member Author

This is blocked by #1086

@davidpdrsn
Copy link
Member Author

I just opened #1161 which should fix this. @lilyball Do you wanna try and test it?

@davidpdrsn davidpdrsn added the E-hard Call for participation: Experience needed to fix: Hard / a lot label Jul 13, 2022
@davidpdrsn davidpdrsn self-assigned this Jul 13, 2022
@davidpdrsn
Copy link
Member Author

I've decided not to support this its not worth the additional complexity it adds. See #1086 (comment).

@davidpdrsn davidpdrsn closed this as not planned Won't fix, can't repro, duplicate, stale Jul 26, 2022
@jplatte
Copy link
Member

jplatte commented Jul 27, 2022

Suggested solution (extended for clarity) so people don't have to click around:

fn router() -> Router {
    Router::new().fallback(...)
}

fn make_some_routes() -> Router {
    router().route("/", ...)
}

fn make_more_routes() -> Router {
    router().route("/", ...)
}

fn my_routes() -> Router {
    router()
        .nest("/a", make_some_routes())
        .nest("/b", make_more_routes())
}

@lilyball
Copy link

If this is really the solution we're going with then please make sure any documentation recommending this explicitly recommends using OriginalUri in the fallback, as copying the fallback onto the nested router will potentially break any fallbacks that are just using the request URI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-axum C-feature-request Category: A feature request, i.e: not implemented / a PR. E-hard Call for participation: Experience needed to fix: Hard / a lot S-blocked Status: marked as blocked ❌ on something else such as a PR or other implementation work.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants