From 1f2c64980465ff92b38b480870e56b94ab927a9d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 30 Nov 2024 17:48:20 +0100 Subject: [PATCH] Document optional extractors --- axum-core/src/extract/option.rs | 5 +-- axum-extra/src/extract/query.rs | 13 ++++++++ axum/src/docs/extract.md | 57 ++++++++++++++++++--------------- axum/src/extract/path/mod.rs | 6 ++++ axum/src/extract/query.rs | 15 ++++++++- 5 files changed, 67 insertions(+), 29 deletions(-) diff --git a/axum-core/src/extract/option.rs b/axum-core/src/extract/option.rs index f628c8bf94..c537e72187 100644 --- a/axum-core/src/extract/option.rs +++ b/axum-core/src/extract/option.rs @@ -6,7 +6,8 @@ use crate::response::IntoResponse; use super::{private, FromRequest, FromRequestParts, Request}; -/// TODO: DOCS +/// Customize the behavior of `Option` as a [`FromRequestParts`] +/// extractor. pub trait OptionalFromRequestParts: Sized { /// If the extractor fails, it will use this "rejection" type. /// @@ -20,7 +21,7 @@ pub trait OptionalFromRequestParts: Sized { ) -> impl Future, Self::Rejection>> + Send; } -/// TODO: DOCS +/// Customize the behavior of `Option` as a [`FromRequest`] extractor. pub trait OptionalFromRequest: Sized { /// If the extractor fails, it will use this "rejection" type. /// diff --git a/axum-extra/src/extract/query.rs b/axum-extra/src/extract/query.rs index 7c73cec38c..6e50456e2f 100644 --- a/axum-extra/src/extract/query.rs +++ b/axum-extra/src/extract/query.rs @@ -18,6 +18,19 @@ use std::fmt; /// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential /// container. /// +/// # `Option>` behavior +/// +/// If `Query` 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>`, 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 diff --git a/axum/src/docs/extract.md b/axum/src/docs/extract.md index b5e8c93eda..20dfe46e9f 100644 --- a/axum/src/docs/extract.md +++ b/axum/src/docs/extract.md @@ -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>) { - 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::{ @@ -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>) { + 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 diff --git a/axum/src/extract/path/mod.rs b/axum/src/extract/path/mod.rs index 2ddcbccfbd..77a61138cb 100644 --- a/axum/src/extract/path/mod.rs +++ b/axum/src/extract/path/mod.rs @@ -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>` behavior +/// +/// You can use `Option>` 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. diff --git a/axum/src/extract/query.rs b/axum/src/extract/query.rs index 68f5bd4ef1..14473aab04 100644 --- a/axum/src/extract/query.rs +++ b/axum/src/extract/query.rs @@ -6,7 +6,20 @@ use serde::de::DeserializeOwned; /// /// `T` is expected to implement [`serde::Deserialize`]. /// -/// # Example +/// # `Option>` behavior +/// +/// If `Query` 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>`, 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::{