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

Middleware that return early break Router::or #380

Closed
Flowneee opened this issue Oct 12, 2021 · 16 comments · Fixed by #409
Closed

Middleware that return early break Router::or #380

Flowneee opened this issue Oct 12, 2021 · 16 comments · Fixed by #409
Labels
C-bug Category: This is a bug.
Milestone

Comments

@Flowneee
Copy link

Bug Report

Version

├── axum v0.2.8

Platform

Linux fdr12.tcsbank.ru 5.14.9-200.fc34.x86_64 #1 SMP Thu Sep 30 11:55:35 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

Description

When I try to split app into multiple Routers (like in docs https://docs.rs/axum/0.2.8/axum/#to-groups-of-routes), layers from "leaf" Routers affect other "leaf" Routers.

I tried this code:

use axum::{
    handler::{get, post, Handler},
    response::{Html, IntoResponse},
    Router,
};
use hyper::StatusCode;
use std::net::SocketAddr;
use tower_http::auth::RequireAuthorizationLayer;

async fn handler_404() -> impl IntoResponse {
    StatusCode::NOT_FOUND
}

#[tokio::main]
async fn main() {
    let foo = Router::new()
        .route("/", get(|| async {}))
        .route("/foo", post(|| async {}));

    let bar = Router::new()
        .route("/requires-auth", get(|| async {}))
        .layer(RequireAuthorizationLayer::basic("a", "a"));

    let app = bar.or(foo).or(handler_404.into_service());  # NOTE: bar is first, not foo

    let addr = "0.0.0.0:8080".parse::<SocketAddr>().unwrap();
    axum::Server::try_bind(&addr)
        .unwrap()
        .serve(app.into_make_service())
        .await;
}

I expected to see this happen: when I do curl localhost:8080/, I get 200 OK

Instead, this happened: 401 Unauthorized.

Basically now all routes require auth from bar "leaf" Router.

If I apply auth layer to handler directly, everything works, but this mean that I cannot apply layer to group of routes, only to all of them at once.

I think that layers should apply only to current Router, at least this is how I understand current documentation.

@davidpdrsn
Copy link
Member

I believe this is the same issue as #348 and is actually a bug with or. Unfortunately we don't have a solution for it yet. The workaround described here should work.

@Flowneee
Copy link
Author

#363 won't fix this problem?

@Flowneee
Copy link
Author

Don't you think that current documentation is misleading then?

@davidpdrsn
Copy link
Member

#363 won't fix this problem?

I don't believe so.

Don't you think that current documentation is misleading then?

You're right, we probably shouldn't use the particular example we do since it wont work.

@Flowneee
Copy link
Author

Flowneee commented Oct 12, 2021

I looked at #363, and (if I understood correctly), now it will

  1. get RouteId by URL;
  2. call router as it was before, but route will be selected by RouteId, not by URL.

If so, I think it is possible to somehow store IDs of routes in Router itself, and before call Router's inners (when handling new request), check, whether Router have current ID or not, so we could skip all Routers but the one we need. Do you think it is viable solution for axum?

@davidpdrsn davidpdrsn mentioned this issue Oct 12, 2021
7 tasks
@davidpdrsn
Copy link
Member

I'm not sure. Made a note to look into it. You're welcome to test it if you want.

@davidpdrsn davidpdrsn changed the title Layer, applied to single Router, affects all other Routers Middleware that return early break Router::or Oct 12, 2021
@davidpdrsn davidpdrsn added the C-bug Category: This is a bug. label Oct 12, 2021
@davidpdrsn davidpdrsn added this to the 0.3 milestone Oct 12, 2021
@davidpdrsn
Copy link
Member

I've tweaked the title a bit to make it more clear what the issue is. I think this is something we should attempt to fix for 0.3.

@davidpdrsn davidpdrsn modified the milestones: 0.3, 0.4 Oct 24, 2021
@oberzs
Copy link

oberzs commented Oct 25, 2021

the same problem is encountered in this code. /ping and /version endpoints work fine without authorization, but the nested /public route does not (was asked to post this here from the tokio Discord):

let no_auth_routes = Router::new()
    .nest(
        "/public",
        service::get(ServeDir::new("./public")).handle_error(|_| {
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Unhandled internal error",
            )
        }),
    )
    .route("/ping", get(get_ping))
    .route("/version", get(get_version));

let auth_routes = Router::new()
    .route("/auth", get(get_auth))
    // the rest of authorized routes
    .layer(RequireAuthorizationLayer::bearer("********"));

let app = no_auth_routes.or(auth_routes);

davidpdrsn added a commit that referenced this issue Oct 25, 2021
With #404 and #402 all routes now have the same types and thus we don't need to nest them but can instead store them all in a map. This simplifies the routing quite a bit and is faster as well.

High level changes:
- Routes are now stored in a `HashMap<RouteId, Route<B>>`.
- `Router::or` is renamed to `Router::merge` because thats what it does now. It copies all routes from one router to another. This also means overlapping routes will cause a panic which is nice win.
- `Router::merge` now only accepts `Router`s so added `Router::fallback` for adding a global 404 handler.
- The `Or` service has been removed.
- `Router::layer` now only adds layers to the routes you actually have meaning middleware runs _after_ routing. I believe that addresses #380 but will test that on another branch.
davidpdrsn added a commit that referenced this issue Oct 25, 2021
@davidpdrsn
Copy link
Member

This was fixed in #408

davidpdrsn added a commit that referenced this issue Oct 25, 2021
* Add test for middleware that return early

Turns out #408 fixed #380.

* changelog
@davidpdrsn
Copy link
Member

davidpdrsn commented Oct 26, 2021

Turns out I was too quick on this one. Fixing this requires doing routing first, before running any middleware. Because if we run the middleware first they might return early and hijack something that would otherwise be a 404. However by running middleware after routing we break other middleware like logging which expect to be called even for 404s... So regardless which path we break something.

I think the best compromise is:

  • Go back to the way it worked in 0.2, which means logging middleware will continue to work
  • Require users to structure their routers like shown here to workaround this issue
  • Or add auth middleware to the handlers individually that need it

I feel this is the best approach for now. Maybe we'll come up with a general solution in the future.

See #422 for more details.

davidpdrsn added a commit that referenced this issue Oct 26, 2021
While thinking about #419 I realized that `main` had a bug where
middleware wouldn't be called if no route matched the incoming request.
`Router` would just directly return a 404 without calling any
middleware.

This fixes by making the fallback default to a service that always
returns 404 and always calling that if no route matches.

Layers applied to the router is then also applied to the fallback.

Unfortunately this breaks #380 but I don't currently see a way to
support both. Auth middleware need to run _after_ routing because you
don't care about auth for unknown paths, but logging middleware need to
run _before_ routing because they do care about seeing requests for
unknown paths so they can log them...

Part of #419
davidpdrsn added a commit that referenced this issue Oct 26, 2021
While thinking about #419 I realized that `main` had a bug where
middleware wouldn't be called if no route matched the incoming request.
`Router` would just directly return a 404 without calling any
middleware.

This fixes by making the fallback default to a service that always
returns 404 and always calling that if no route matches.

Layers applied to the router is then also applied to the fallback.

Unfortunately this breaks #380 but I don't currently see a way to
support both. Auth middleware need to run _after_ routing because you
don't care about auth for unknown paths, but logging middleware need to
run _before_ routing because they do care about seeing requests for
unknown paths so they can log them...

Part of #419
@Flowneee
Copy link
Author

Flowneee commented Nov 3, 2021

I think renaming or to merge kind of solves problem, because (at least for me) semantically mergeis more like concatenation of two lists, and that is what merge do exactly - it make single list from 2 lists of Servicees (which Router is roughly), and or is more like creating tree-like structure. nest is also creating tree IMO, which was slightly confusing.

But I think it would be cool to have something like nest, but with non-unique prefix, so you can mount 2 Routers under single prefix and processing request would be like this:

  • check routes in first Router. If successful - proceed with first Router, if not - next step;
  • check routes in second Router. If successful - proceed with second Router, if not - repeat until you run out of Routers with current prefix;
  • if none matched, proceed as 404 or whatever default route is.

This will allow to create tree-like structure of Routers, where layers can be applied only to certain routes, which do not share common prefix.

@davidpdrsn
Copy link
Member

How is that different from .nest("/foo", router_a.merge(router_b))?

@Flowneee
Copy link
Author

Flowneee commented Nov 3, 2021

Layers from both routers will mix again. What I think is something like

let router_a = Router::new().route("/metrics", ...).route("/health", ...);
let router_b = Router::new().route("/shutdown", ...).route("/kill", ...).layer(SomeAuthLayer {});

let router = Route::new().nest("/manage", router_a).nest("/manage", router_b);

Ofc there is always option to apply layers to handlers directly, even though it is verbose.

@davidpdrsn
Copy link
Member

Layers from both routers will mix again

What do mean by this? Layers only apply to routes added to the router at the time the layer is added.

@Flowneee
Copy link
Author

Flowneee commented Nov 3, 2021

Seems I got it wrong. If replace or with merge, on new axum version code from first message works as expected

use axum::{
    handler::Handler,
    response::IntoResponse,
    routing::{get, post},
    Router,
};
use hyper::StatusCode;
use std::net::SocketAddr;
use tower_http::auth::RequireAuthorizationLayer;

async fn handler_404() -> impl IntoResponse {
    StatusCode::NOT_FOUND
}

#[tokio::main]
async fn main() {
    let foo = Router::new()
        .route("/", get(|| async {}))
        .route("/foo", post(|| async {}));

    let bar = Router::new()
        .route("/requires-auth", get(|| async {}))
        .layer(RequireAuthorizationLayer::basic("a", "a"));

    let app = bar.merge(foo).fallback(handler_404.into_service()); //NOTE: bar is first, not foo

    let addr = "0.0.0.0:8080".parse::<SocketAddr>().unwrap();
    axum::Server::try_bind(&addr)
        .unwrap()
        .serve(app.into_make_service())
        .await;
}

Thanks!

@davidpdrsn
Copy link
Member

A proper fix for this was released in 0.3.2: Route::route_layer which applies a middleware that only runs if the request matches a route. This means middleware that return early wont hijack 404s.

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

Successfully merging a pull request may close this issue.

3 participants