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

add method_not_allowed_fallback to router #2903

Merged
merged 24 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
73ddf83
add method_not_allowed_fallback to router
Lachstec Sep 8, 2024
833daf8
add documentation for method fallback
Lachstec Sep 8, 2024
0617c75
add docs attribute to method fallback
Lachstec Sep 8, 2024
53d75a4
add method_not_allowed_fallback to router
Lachstec Sep 8, 2024
8f97008
add documentation for method fallback
Lachstec Sep 8, 2024
3aedc9a
add docs attribute to method fallback
Lachstec Sep 8, 2024
ce7a42f
Merge branch 'feat/method_fallback' of github.com:Lachstec/axum into …
Lachstec Sep 27, 2024
ff942bc
add no_run to docs for method_not_allowed_fallback
Lachstec Sep 27, 2024
61237da
add option to set fallback if no custom one exists
Lachstec Sep 29, 2024
19b13bb
change method_not_allowed_fallback to use default_fallback
Lachstec Sep 29, 2024
d59d203
switch conditions in match on default fallback
Lachstec Sep 29, 2024
4645f66
test that method_not_allowed_fallback respects previously set fallback
Lachstec Sep 29, 2024
7d2be52
cargo fmt
Lachstec Sep 29, 2024
10c16df
remove typo from docs
Lachstec Sep 29, 2024
bcb2579
remove unnecessary mut
Lachstec Sep 29, 2024
6cd8386
add test for method not allowed fallback with state
Lachstec Sep 29, 2024
6a0d9a2
add another test for method_not_allowed_fallback
Lachstec Oct 11, 2024
546458e
Merge branch 'main' into feat/method_fallback
Lachstec Oct 11, 2024
61f3460
use tap_inner macro and fix tests
Lachstec Oct 11, 2024
a6882c6
Adjust grammar and formatting
Lachstec Oct 12, 2024
da082a5
Better explain how method_not_allowed_fallback works
Lachstec Oct 12, 2024
5c384df
improve grammar and wording
Lachstec Oct 12, 2024
733f0aa
change handle_405 to default_fallback
Lachstec Oct 12, 2024
190a84a
Update axum/src/docs/routing/method_not_allowed_fallback.md
jplatte Oct 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions axum/src/docs/routing/method_not_allowed_fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Add a fallback [`Handler`] for the case where a route exists, but the method of the request is not supported.

Sets a fallback on all previously registered [`MethodRouter`]s,
to be called when no matching method handler is set.

```rust,no_run
use axum::{response::IntoResponse, routing::get, Router};

async fn hello_world() -> impl IntoResponse {
"Hello, world!\n"
}

async fn default_fallback() -> impl IntoResponse {
"Default fallback\n"
}

async fn handle_405() -> impl IntoResponse {
"Method not allowed fallback"
}

#[tokio::main]
async fn main() {
let router = Router::new()
.route("/", get(hello_world))
.fallback(default_fallback)
.method_not_allowed_fallback(handle_405);

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();

axum::serve(listener, router).await.unwrap();
}
```

The fallback only applies if there is a `MethodRouter` registered for a given path,
but the method used in the request is not specified. In the example, a `GET` on
`http://localhost:3000` causes the `hello_world` handler to react, while issuing a
`POST` triggers `handle_405`. Calling an entirely different route, like `http://localhost:3000/hello`
causes `default_fallback` to run.
13 changes: 13 additions & 0 deletions axum/src/routing/method_routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,19 @@ where
self.fallback = Fallback::BoxedHandler(BoxedIntoRoute::from_handler(handler));
self
}

/// Add a fallback [`Handler`] if no custom one has been provided.
pub(crate) fn default_fallback<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
S: Send + Sync + 'static,
{
match self.fallback {
Fallback::Default(_) => self.fallback(handler),
_ => self,
}
}
}

impl MethodRouter<(), Infallible> {
Expand Down
12 changes: 12 additions & 0 deletions axum/src/routing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,18 @@ where
.fallback_endpoint(Endpoint::Route(route))
}

#[doc = include_str!("../docs/routing/method_not_allowed_fallback.md")]
pub fn method_not_allowed_fallback<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
tap_inner!(self, mut this => {
this.path_router
.method_not_allowed_fallback(handler.clone())
})
}

fn fallback_endpoint(self, endpoint: Endpoint<S>) -> Self {
tap_inner!(self, mut this => {
this.fallback_router.set_fallback(endpoint);
Expand Down
17 changes: 16 additions & 1 deletion axum/src/routing/path_router.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::extract::{nested_path::SetNestedPath, Request};
use crate::{
extract::{nested_path::SetNestedPath, Request},
handler::Handler,
};
use axum_core::response::IntoResponse;
use matchit::MatchError;
use std::{borrow::Cow, collections::HashMap, convert::Infallible, fmt, sync::Arc};
Expand Down Expand Up @@ -110,6 +113,18 @@ where
Ok(())
}

pub(super) fn method_not_allowed_fallback<H, T>(&mut self, handler: H)
where
H: Handler<T, S>,
T: 'static,
{
for (_, endpoint) in self.routes.iter_mut() {
if let Endpoint::MethodRouter(rt) = endpoint {
*rt = rt.clone().default_fallback(handler.clone());
}
}
}

pub(super) fn route_service<T>(
&mut self,
path: &str,
Expand Down
45 changes: 45 additions & 0 deletions axum/src/routing/tests/fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,51 @@ async fn merge_router_with_fallback_into_empty() {
assert_eq!(res.text().await, "outer");
}

#[crate::test]
async fn mna_fallback_with_existing_fallback() {
let app = Router::new()
.route(
"/",
get(|| async { "test" }).fallback(|| async { "index fallback" }),
)
.route("/path", get(|| async { "path" }))
.method_not_allowed_fallback(|| async { "method not allowed fallback" });

let client = TestClient::new(app);
let index_fallback = client.post("/").await;
let method_not_allowed_fallback = client.post("/path").await;

assert_eq!(index_fallback.text().await, "index fallback");
assert_eq!(
method_not_allowed_fallback.text().await,
"method not allowed fallback"
);
}

#[crate::test]
async fn mna_fallback_with_state() {
let app = Router::new()
.route("/", get(|| async { "index" }))
.method_not_allowed_fallback(|State(state): State<&'static str>| async move { state })
.with_state("state");

let client = TestClient::new(app);
let res = client.post("/").await;
assert_eq!(res.text().await, "state");
}

#[crate::test]
async fn mna_fallback_with_unused_state() {
let app = Router::new()
.route("/", get(|| async { "index" }))
.with_state(())
.method_not_allowed_fallback(|| async move { "bla" });

let client = TestClient::new(app);
let res = client.post("/").await;
assert_eq!(res.text().await, "bla");
}

#[crate::test]
async fn state_isnt_cloned_too_much_with_fallback() {
let state = CountingCloneableState::new();
Expand Down