Skip to content

Commit

Permalink
Document optional extractors
Browse files Browse the repository at this point in the history
  • Loading branch information
jplatte committed Nov 30, 2024
1 parent a8e6136 commit 1f2c649
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 29 deletions.
5 changes: 3 additions & 2 deletions axum-core/src/extract/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use crate::response::IntoResponse;

use super::{private, FromRequest, FromRequestParts, Request};

/// TODO: DOCS
/// Customize the behavior of `Option<Self>` as a [`FromRequestParts`]
/// extractor.
pub trait OptionalFromRequestParts<S>: Sized {
/// If the extractor fails, it will use this "rejection" type.
///
Expand All @@ -20,7 +21,7 @@ pub trait OptionalFromRequestParts<S>: Sized {
) -> impl Future<Output = Result<Option<Self>, Self::Rejection>> + Send;
}

/// TODO: DOCS
/// Customize the behavior of `Option<Self>` as a [`FromRequest`] extractor.
pub trait OptionalFromRequest<S, M = private::ViaRequest>: Sized {
/// If the extractor fails, it will use this "rejection" type.
///
Expand Down
13 changes: 13 additions & 0 deletions axum-extra/src/extract/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ use std::fmt;
/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
/// container.
///
/// # `Option<Query<T>>` behavior
///
/// If `Query<T>` itself is used as an extractor and there is no query string in
/// the request URL, `T`'s `Deserialize` implementation is called on an empty
/// string instead.
///
/// You can avoid this by using `Option<Query<T>>`, which gives you `None` in
/// the case that there is no query string in the request URL.
///
/// Note that an empty query string is not the same as no query string, that is
/// `https://example.org/` and `https://example.org/?` are not treated the same
/// in this case.
///
/// # Example
///
/// ```rust,no_run
Expand Down
57 changes: 31 additions & 26 deletions axum/src/docs/extract.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,29 +200,11 @@ async fn handler(
axum enforces this by requiring the last extractor implements [`FromRequest`]
and all others implement [`FromRequestParts`].

# Optional extractors

TODO: Docs, more realistic example

```rust,no_run
use axum::{routing::post, Router};
use axum_extra::{headers::UserAgent, TypedHeader};
use serde_json::Value;
async fn foo(user_agent: Option<TypedHeader<UserAgent>>) {
if let Some(TypedHeader(user_agent)) = user_agent {
// The client sent a user agent
} else {
// No user agent header
}
}
# Handling extractor rejections

let app = Router::new().route("/foo", post(foo));
# let _: Router = app;
```

Wrapping extractors in `Result` makes them optional and gives you the reason
the extraction failed:
If you want to handle the case of an extractor failing within a specific
handler, you can wrap it in `Result`, with the error being the rejection type
of the extractor:

```rust,no_run
use axum::{
Expand Down Expand Up @@ -261,10 +243,33 @@ let app = Router::new().route("/users", post(create_user));
# let _: Router = app;
```

Another option is to make use of the optional extractors in [axum-extra] that
either returns `None` if there are no query parameters in the request URI,
or returns `Some(T)` if deserialization was successful.
If the deserialization was not successful, the request is rejected.
# Optional extractors

Some extractors implement [`OptionalFromRequestParts`] in addition to
[`FromRequestParts`], or [`OptionalFromRequest`] in addition to [`FromRequest`].

These extractors can be used inside of `Option`. It depends on the particular
`OptionalFromRequestParts` or `OptionalFromRequest` implementation what this
does: For example for `TypedHeader` from axum-extra, you get `None` if the
header you're trying to extract is not part of the request, but if the header
is present and fails to parse, the request is rejected.

```rust,no_run
use axum::{routing::post, Router};
use axum_extra::{headers::UserAgent, TypedHeader};
use serde_json::Value;
async fn foo(user_agent: Option<TypedHeader<UserAgent>>) {
if let Some(TypedHeader(user_agent)) = user_agent {
// The client sent a user agent
} else {
// No user agent header
}
}
let app = Router::new().route("/foo", post(foo));
# let _: Router = app;
```

# Customizing extractor responses

Expand Down
6 changes: 6 additions & 0 deletions axum/src/extract/path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ use std::{fmt, sync::Arc};
/// parameters must be valid UTF-8, otherwise `Path` will fail and return a `400
/// Bad Request` response.
///
/// # `Option<Path<T>>` behavior
///
/// You can use `Option<Path<T>>` as an extractor to allow the same handler to
/// be used in a route with parameters that deserialize to `T`, and another
/// route with no parameters at all.
///
/// # Example
///
/// These examples assume the `serde` feature of the [`uuid`] crate is enabled.
Expand Down
15 changes: 14 additions & 1 deletion axum/src/extract/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,20 @@ use serde::de::DeserializeOwned;
///
/// `T` is expected to implement [`serde::Deserialize`].
///
/// # Example
/// # `Option<Query<T>>` behavior
///
/// If `Query<T>` itself is used as an extractor and there is no query string in
/// the request URL, `T`'s `Deserialize` implementation is called on an empty
/// string instead.
///
/// You can avoid this by using `Option<Query<T>>`, which gives you `None` in
/// the case that there is no query string in the request URL.
///
/// Note that an empty query string is not the same as no query string, that is
/// `https://example.org/` and `https://example.org/?` are not treated the same
/// in this case.
///
/// # Examples
///
/// ```rust,no_run
/// use axum::{
Expand Down

0 comments on commit 1f2c649

Please sign in to comment.