[go: up one dir, main page]

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

Allow route fallback #257

Closed
jplatte opened this issue Aug 24, 2021 · 8 comments
Closed

Allow route fallback #257

jplatte opened this issue Aug 24, 2021 · 8 comments
Labels
C-feature-request Category: A feature request, i.e: not implemented / a PR.

Comments

@jplatte
Copy link
Member
jplatte commented Aug 24, 2021

Feature Request

I'd like to be able to use .or() or something similar to fall back to another route when an extractor (in particular a path extractor) fails, or a tower_http::services::ServeDir instance didn't find a matching file. Currently, the handler supplied to .or() is only used when a route didn't match by the first parameter to .route(), and when adding multiple .or()s onto each other only one is ever used (I think).

Motivation

https://turbo.fish has its static assets right next to dynamic routes. I think this is not a bad design: Since the dynamic routes only ever look like ::<_> or <_>:: (underscore being a wildcard here), there is never an ambiguity with static files in practice and nesting static files under something like /static/ would prevent me from adding favicon.ico to the root in a straight-forward manner, which is a best practice. Also I like the shorter paths when there is no ambiguity anyways.

Proposal

Have .or() call the given service not only if no other route matched, but also if a route's path extractor failed, or an inner service like tower_http::services::ServeDir returns some sort of not-found rejection.

Alternatives

🤷

@jplatte jplatte changed the title Allow route fallback on extractor failure Allow route fallback Aug 24, 2021
@davidpdrsn davidpdrsn added the C-feature-request Category: A feature request, i.e: not implemented / a PR. label Aug 24, 2021
@davidpdrsn
Copy link
Member

I think allowing extractors to reject and request and forward to the next route is a good idea. Its very similar to rocket's request guards. I haven't tested it yet but I think that should be possible to implement.

However I don't it is possible for .or(ServeDir) to be able to "reject" a request if it doesn't find a matching file. The reason has to do with how or is implemented:

  • Fundamentally or has to try and call one service and if that service couldn't handle the request, it needs to call another service with that same request.
  • Since tower::Service requires ownership over the request, calling a service means giving up the request. So or cannot "just" call another service with the same request, because the request has been moved.
  • The trick that makes it work is that if an axum router couldn't handle a request an EmptyRouter service will always be called. This type is always at the bottom of your router stacks. It will insert the request into a response extension (done here).
  • This means when or gets the response back from the first service it is able to know that the request couldn't be handled, as supposed to a handler returning 404, and recover the request. This happens here.
  • However the type used by EmptyRouter to pass the request back is private (FromEmptyRouter) meaning other services defined outside axum cannot use it.
  • There ServeDir has no way of telling or that it couldn't handle the request and thus .or(ServeDir) will always match all requests.

I haven't thought deeply about if there is a way around it.

@jplatte
Copy link
Member Author
jplatte commented Aug 24, 2021

Sounds to me like 404 handlers need to be special-cased afterall, then there is no expectation that they will receive the original request 😉

Maybe my 404 handler will just have to be defined like the one in the previous 404 example (MapResponse catching method not allowed / not found and setting the custom body) once I can use .or(ServeDir) instead of .nest(ServeDir) (which I can do once extractors can reject with "please try next route").

A related problem: When extractors are allowed to forward a request on failure, the next handler might fail because it tries to take the request body when that was already taken from the first handler. I'd assume extractors are applied in-order, so should the docs encourage putting body extractors last?

@davidpdrsn
Copy link
Member

When extractors are allowed to forward a request on failure, the next handler might fail because it tries to take the request body when that was already taken from the first handler. I'd assume extractors are applied in-order, so should the docs encourage putting body extractors last?

Yep that would certainly be an issue. I know warp has the same issue where filters can reject requests in this way. Warp returns a 500 if you attempt to consume the request body twice. So yeah we'd have to document that.

@davidpdrsn
Copy link
Member

I think allowing extractors to reject and request and forward to the next route is a good idea. Its very similar to rocket's request guards. I haven't tested it yet but I think that should be possible to implement.

I just thought of something. If extractors failing will automatically fallback and try the next route it would cause issues with

Router::new()
    // route that accepts all requests
    .nest("/", _)
    // `Auth ` is an extractor that authorizes requests
    .route("/", get(|_: Auth| async { });

This would cause unauthorized requests to fallback to the nested route, which probably isn't what you'd want.

Assuming we have wildcard routes like .route("/*", _) we'd have the same issue.

@jplatte
Copy link
Member Author
jplatte commented Aug 25, 2021

Yeah, that's why I only proposed the path extractor changing its behavior. I don't think falling back on something like an invalid JSON payload, missing auth header or non-UTF8 query parameters makes any sense.

@davidpdrsn
Copy link
Member

Yeah thats true. I wonder if there is a way for extractors to explicitly opt-in to fallback/forwarding. Maybe by returning a special error type 🤔

@jplatte
Copy link
Member Author
jplatte commented Aug 25, 2021

Yeah, that's roughly what I was thinking. Should probably expose it too so user-defined extractors can opt into the same behavior.

@davidpdrsn
Copy link
Member

With #408 we now have Router::fallback and Router::or has been renamed to merge and will now literally merge the two routers. No more complex rerouting based on which leaf receive received the request 🥳

While that doesn't allow for extractors to cause fallback I believe this is enough to close this issue.

I do think fallback extractors is an interesting idea but we also have to consider if it could lead to overly complex routers. Also not quite sure how it would work with the new matchit router. Realistically not something axum will support soon but feel free to open a new issue specifically about that to discuss it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-feature-request Category: A feature request, i.e: not implemented / a PR.
Projects
None yet
Development

No branches or pull requests

2 participants