-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Support fallbacks for nested routers #1161
Support fallbacks for nested routers #1161
Conversation
Co-authored-by: Jonas Platte <[email protected]>
When [fallbacks] are called differs between `nest` and `nested_service`. Routers | ||
nested with `nest` will delegate to the outer router's fallback if they don't | ||
have a matching route, whereas `nested_service` will not. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in #1086 (comment), these docs need to be updated to describe how nested routers handle fallbacks, as well as the following example (which uses nest_service
to allow the nested router to have a fallback).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial review, will come back to this.
edit: Oh, I thought this was #1086 😅
When [fallbacks] are called differs between `nest` and `nested_service`. Routers | ||
nested with `nest` will delegate to the outer router's fallback if they don't | ||
have a matching route, whereas `nested_service` will not. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When [fallbacks] are called differs between `nest` and `nested_service`. Routers | |
nested with `nest` will delegate to the outer router's fallback if they don't | |
have a matching route, whereas `nested_service` will not. | |
When [fallbacks] are called differs between `nest` and `nest_service`. Routers | |
nested with `nest` will delegate to the outer router's fallback if they don't | |
have a matching route, whereas `nest_service` will not. |
// the fallback is not called for request starting with `/assets` but will be | ||
// called for requests starting with `/api` if `nested_router` doesn't have | ||
// a matching route | ||
.fallback(fallback.into_service()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be correct to add this? It's the semantics I would intuitively expect.
// the fallback is not called for request starting with `/assets` but will be | |
// called for requests starting with `/api` if `nested_router` doesn't have | |
// a matching route | |
.fallback(fallback.into_service()); | |
// the fallback is not called for request starting with `/assets` but will be | |
// called for requests starting with `/api` if `nested_router` doesn't have | |
// a matching route, or its own fallback service | |
.fallback(fallback.into_service()); |
|
||
# Example | ||
|
||
`nest_service` can for example be used with [`tower_http::services::ServeDir`] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know where the link definition for this is, but we should make sure to link to a specific version of tower-http, given the example below is going to stop working with the next breaking release (due to io::Error
=> Infallible
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My intention is so somewhat coordinate the next release of axum and tower-http but yeah will need to be updated.
Hehe yes #1086 need to be merged first as this builds that. |
routes, | ||
custom_fallbacks, | ||
// We consider the router on the right a "sub router" and therefore we always discards | ||
// its default fallback. Custom fallbacks will be nested. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also worth trying to convert a layered default fallback into a custom fallback so it's not lost.
But have to think about the edge cases and what might be least surprising.
I'm starting to think that maybe we shouldn't even support nested routers calling the outer routers fallback if they don't have their own. It's nice if you have one function that builds something like an API router to be able to nest that all at once. And it's a little inconvenient having to add the fallback both to the api and outer routers. However it definitely is simpler to just not support it. Both implementation wise but also might be easier to understand. That way all services would be nested "opaquely" and require their own fallbacks. And we'd only have one |
If |
Yeah that was the only reason 😕 I've come around as well. Will change it when I'm back from vacation. |
Not supporting fallbacks from a nested router to the outer router would be a shame though. Right now I can use nested routers to split up my routes into a hierarchy, like nesting I do agree that there is some potential for confusion between "nested router with a configured fallback" and "nested router without a fallback", but that can be handled via documentation. Besides just documenting that nested routers without a fallback will defer to their parent router for fallback behavior, we could also emphasize the fact that a fallback acts as a low-priority catch-all route. It should be pretty obvious what the difference is between nesting a router that only has specific routes vs nesting one that has a catch-all route, and so that would help form an intuition about how nested fallbacks work. All of this to say, while at the moment I'm not relying on nested router fallback behavior, it's how I was planning on structuring more complicated router setups in the future. |
I agree it is unfortunate and that exact use case I do wanna support, just not sure it's worth this additional complexity. If we were to drop this change and require explicit fallbacks on nested routers, how many places would you have to set a fallback in your code specifically? If it is only for the I personally don't like building routers out of more than one level of nested routers but I'm sure some people split things up a lot. |
I'd have to set it on every nested router I use that doesn't already have its own fallback. So that really depends on my route structure. My inclination right now is if I have a hierarchical route structure I might want to actually use a separate If I have only one level of nesting then I can conceivably require the root to apply the same fallback to each router it nests, but the moment I have more than one level of nesting that breaks and I'd have to explicitly pass the global fallback to every single function so it can attach that to its own router. That's doable but rather annoying. And a hierarchical structure feels like a common thing to find in a complex API.
How much additional complexity is for fallback behavior? My impression is that most of the complexity of nesting routers comes from merging the nested router's routes into the parent router. I'm assuming the only reason you don't just convert the fallback on the nested router into a catch-all route is because of |
I've decided not to support this its not worth the additional complexity it adds. See #1086 (comment). |
This branch includes #1086 so that should be reviewed and merged first.
This reworks how fallbacks are implemented to make them more consistent with
regular routes and fix #1053.
The big win is that nested fallbacks "just work":
I also made one breaking change for merging routers that only have "default
fallbacks" is done.
A default fallback is the one used when you haven't added a fallback with
Router::fallback
. So if you doRouter::new()
that router has a defaultfallback.
Router::new().layer(...)
adds a middleware to the default fallback.When you merge routers that only have default fallbacks (regardless of
middleware) the combined router will have the fallback from the left hand side
router. I.e.:
I think this is the least surprising behavior since the routers you merge tends
to be "sub routers" so having their fallbacks overwrite the ones on the outer
router is surprising I think.
All this only applies to default fallbacks. Any custom fallbacks added with
Router::fallback
are merged as normal and will panic if there are conflicts.Additionally adding a fallback twice will now panic
This wouldn't panic previously and was inconsistent.
Fixes #1053
TODO