Skip to content

Commit cae6bc3

Browse files
authored
axum: Use serde_html_form for Query and Form (#3594)
1 parent 061666a commit cae6bc3

File tree

13 files changed

+73
-262
lines changed

13 files changed

+73
-262
lines changed

Cargo.lock

Lines changed: 15 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

axum-extra/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ with-rejection = ["dep:axum"]
9797

9898
# Enabled by docs.rs because it uses all-features
9999
# Enables upstream things linked to in docs
100-
__private_docs = ["axum/json", "dep:serde", "dep:tower"]
100+
__private_docs = ["axum/json", "axum/query", "dep:serde", "dep:tower"]
101101

102102
[dependencies]
103103
axum-core = { path = "../axum-core", version = "0.5.5" }
@@ -124,6 +124,11 @@ percent-encoding = { version = "2.1", optional = true }
124124
prost = { version = "0.14", optional = true }
125125
rustversion = { version = "1.0.9", optional = true }
126126
serde_core = { version = "1.0.221", optional = true }
127+
# DO NOT update. axum itself uses serde_html_form 0.3.x which has slightly
128+
# different behavior in some edge cases. This feature here is kept (deprecated)
129+
# to let people transition to that easier (fewer breaking changes to deal with
130+
# at once if they keep using axum_extra's `Query` / `Form` initially when going
131+
# to axum 0.9).
127132
serde_html_form = { version = "0.2.0", optional = true }
128133
serde_json = { version = "1.0.71", optional = true }
129134
serde_path_to_error = { version = "0.1.8", optional = true }

axum-extra/src/extract/form.rs

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![allow(deprecated)]
2+
13
use axum::extract::rejection::RawFormRejection;
24
use axum::{
35
extract::{FromRequest, RawForm, Request},
@@ -12,31 +14,16 @@ use serde_core::de::DeserializeOwned;
1214
///
1315
/// `T` is expected to implement [`serde::Deserialize`].
1416
///
15-
/// # Differences from `axum::extract::Form`
16-
///
17-
/// This extractor uses [`serde_html_form`] under-the-hood which supports multi-value items. These
18-
/// are sent by multiple `<input>` attributes of the same name (e.g. checkboxes) and `<select>`s
19-
/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
20-
/// container.
21-
///
22-
/// # Example
23-
///
24-
/// ```rust,no_run
25-
/// use axum_extra::extract::Form;
26-
/// use serde::Deserialize;
27-
///
28-
/// #[derive(Deserialize)]
29-
/// struct Payload {
30-
/// #[serde(rename = "value")]
31-
/// values: Vec<String>,
32-
/// }
17+
/// # Deprecated
3318
///
34-
/// async fn accept_form(Form(payload): Form<Payload>) {
35-
/// // ...
36-
/// }
37-
/// ```
19+
/// This extractor used to use a different deserializer under-the-hood but that
20+
/// is no longer the case. Now it only uses an older version of the same
21+
/// deserializer, purely for ease of transition to the latest version.
22+
/// Before switching to `axum::extract::Form`, it is recommended to read the
23+
/// [changelog for `serde_html_form v0.3.0`][changelog].
3824
///
39-
/// [`serde_html_form`]: https://crates.io/crates/serde_html_form
25+
/// [changelog]: https://github.com/jplatte/serde_html_form/blob/main/CHANGELOG.md#030
26+
#[deprecated = "see documentation"]
4027
#[derive(Debug, Clone, Copy, Default)]
4128
#[cfg(feature = "form")]
4229
pub struct Form<T>(pub T);
@@ -90,6 +77,7 @@ composite_rejection! {
9077
/// Rejection used for [`Form`].
9178
///
9279
/// Contains one variant for each way the [`Form`] extractor can fail.
80+
#[deprecated = "because Form is deprecated"]
9381
pub enum FormRejection {
9482
RawFormRejection,
9583
FailedToDeserializeForm,

axum-extra/src/extract/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ pub use self::cookie::PrivateCookieJar;
5252
pub use self::cookie::SignedCookieJar;
5353

5454
#[cfg(feature = "form")]
55+
#[allow(deprecated)]
5556
pub use self::form::{Form, FormRejection};
5657

5758
#[cfg(feature = "query")]
5859
pub use self::query::OptionalQuery;
5960
#[cfg(feature = "query")]
61+
#[allow(deprecated)]
6062
pub use self::query::{OptionalQueryRejection, Query, QueryRejection};
6163

6264
#[cfg(feature = "multipart")]

axum-extra/src/extract/query.rs

Lines changed: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![allow(deprecated)]
2+
13
use axum_core::__composite_rejection as composite_rejection;
24
use axum_core::__define_rejection as define_rejection;
35
use axum_core::extract::FromRequestParts;
@@ -8,72 +10,16 @@ use serde_core::de::DeserializeOwned;
810
///
911
/// `T` is expected to implement [`serde::Deserialize`].
1012
///
11-
/// # Differences from `axum::extract::Query`
12-
///
13-
/// This extractor uses [`serde_html_form`] under-the-hood which supports multi-value items. These
14-
/// are sent by multiple `<input>` attributes of the same name (e.g. checkboxes) and `<select>`s
15-
/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
16-
/// container.
17-
///
18-
/// # Example
19-
///
20-
/// ```rust,no_run
21-
/// use axum::{routing::get, Router};
22-
/// use axum_extra::extract::Query;
23-
/// use serde::Deserialize;
24-
///
25-
/// #[derive(Deserialize)]
26-
/// struct Pagination {
27-
/// page: usize,
28-
/// per_page: usize,
29-
/// }
30-
///
31-
/// // This will parse query strings like `?page=2&per_page=30` into `Pagination`
32-
/// // structs.
33-
/// async fn list_things(pagination: Query<Pagination>) {
34-
/// let pagination: Pagination = pagination.0;
13+
/// # Deprecated
3514
///
36-
/// // ...
37-
/// }
15+
/// This extractor used to use a different deserializer under-the-hood but that
16+
/// is no longer the case. Now it only uses an older version of the same
17+
/// deserializer, purely for ease of transition to the latest version.
18+
/// Before switching to `axum::extract::Form`, it is recommended to read the
19+
/// [changelog for `serde_html_form v0.3.0`][changelog].
3820
///
39-
/// let app = Router::new().route("/list_things", get(list_things));
40-
/// # let _: Router = app;
41-
/// ```
42-
///
43-
/// If the query string cannot be parsed it will reject the request with a `400
44-
/// Bad Request` response.
45-
///
46-
/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
47-
/// example.
48-
///
49-
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
50-
///
51-
/// While `Option<T>` will handle empty parameters (e.g. `param=`), beware when using this with a
52-
/// `Vec<T>`. If your list is optional, use `Vec<T>` in combination with `#[serde(default)]`
53-
/// instead of `Option<Vec<T>>`. `Option<Vec<T>>` will handle 0, 2, or more arguments, but not one
54-
/// argument.
55-
///
56-
/// # Example
57-
///
58-
/// ```rust,no_run
59-
/// use axum::{routing::get, Router};
60-
/// use axum_extra::extract::Query;
61-
/// use serde::Deserialize;
62-
///
63-
/// #[derive(Deserialize)]
64-
/// struct Params {
65-
/// #[serde(default)]
66-
/// items: Vec<usize>,
67-
/// }
68-
///
69-
/// // This will parse 0 occurrences of `items` as an empty `Vec`.
70-
/// async fn process_items(Query(params): Query<Params>) {
71-
/// // ...
72-
/// }
73-
///
74-
/// let app = Router::new().route("/process_items", get(process_items));
75-
/// # let _: Router = app;
76-
/// ```
21+
/// [changelog]: https://github.com/jplatte/serde_html_form/blob/main/CHANGELOG.md#030
22+
#[deprecated = "see documentation"]
7723
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
7824
#[derive(Debug, Clone, Copy, Default)]
7925
pub struct Query<T>(pub T);
@@ -140,14 +86,15 @@ composite_rejection! {
14086
/// Rejection used for [`Query`].
14187
///
14288
/// Contains one variant for each way the [`Query`] extractor can fail.
89+
#[deprecated = "because Query is deprecated"]
14390
pub enum QueryRejection {
14491
FailedToDeserializeQueryString,
14592
}
14693
}
14794

14895
/// Extractor that deserializes query strings into `None` if no query parameters are present.
14996
///
150-
/// Otherwise behaviour is identical to [`Query`].
97+
/// Otherwise behaviour is identical to [`Query`][axum::extract::Query].
15198
/// `T` is expected to implement [`serde::Deserialize`].
15299
///
153100
/// # Example
@@ -179,11 +126,6 @@ composite_rejection! {
179126
///
180127
/// If the query string cannot be parsed it will reject the request with a `400
181128
/// Bad Request` response.
182-
///
183-
/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
184-
/// example.
185-
///
186-
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
187129
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
188130
#[derive(Debug, Clone, Copy, Default)]
189131
pub struct OptionalQuery<T>(pub Option<T>);

axum-extra/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@
1818
//! `cookie-key-expansion` | Enables the [`Key::derive_from`](crate::extract::cookie::Key::derive_from) method |
1919
//! `erased-json` | Enables the [`ErasedJson`](crate::response::ErasedJson) response |
2020
//! `error-response` | Enables the [`InternalServerError`](crate::response::InternalServerError) response |
21-
//! `form` | Enables the [`Form`](crate::extract::Form) extractor |
21+
//! `form` (deprecated) | Enables the [`Form`](crate::extract::Form) extractor |
2222
//! `handler` | Enables the [handler] utilities |
2323
//! `json-deserializer` | Enables the [`JsonDeserializer`](crate::extract::JsonDeserializer) extractor |
2424
//! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response |
2525
//! `middleware` | Enables the [middleware] utilities |
2626
//! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor |
2727
//! `optional-path` | Enables the [`OptionalPath`](crate::extract::OptionalPath) extractor |
2828
//! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response |
29-
//! `query` | Enables the [`Query`](crate::extract::Query) extractor |
29+
//! `query` (deprecated) | Enables the [`Query`](crate::extract::Query) extractor |
3030
//! `routing` | Enables the [routing] utilities |
3131
//! `tracing` | Log rejections from built-in extractors | <span role="img" aria-label="Default feature">✔</span>
3232
//! `typed-routing` | Enables the [`TypedPath`](crate::routing::TypedPath) routing utilities and the `routing` feature. |

axum/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,15 @@ default = [
4949
"tower-log",
5050
"tracing",
5151
]
52-
form = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
52+
form = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"]
5353
http1 = ["dep:hyper", "hyper?/http1", "hyper-util?/http1"]
5454
http2 = ["dep:hyper", "hyper?/http2", "hyper-util?/http2"]
5555
json = ["dep:serde_json", "dep:serde_path_to_error"]
5656
macros = ["dep:axum-macros"]
5757
matched-path = []
5858
multipart = ["dep:multer"]
5959
original-uri = []
60-
query = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
60+
query = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"]
6161
tokio = [
6262
"dep:hyper-util",
6363
"dep:tokio",
@@ -120,9 +120,9 @@ hyper = { version = "1.4.0", optional = true }
120120
hyper-util = { version = "0.1.4", features = ["tokio", "server", "service"], optional = true }
121121
multer = { version = "3.0.0", optional = true }
122122
reqwest = { version = "0.12", optional = true, default-features = false, features = ["json", "stream", "multipart"] }
123+
serde_html_form = { version = "0.3.2", optional = true }
123124
serde_json = { version = "1.0", features = ["raw_value"], optional = true }
124125
serde_path_to_error = { version = "0.1.8", optional = true }
125-
serde_urlencoded = { version = "0.7", optional = true }
126126
sha1 = { version = "0.10", optional = true }
127127
tokio = { package = "tokio", version = "1.44", features = ["time"], optional = true }
128128
tokio-tungstenite = { version = "0.28.0", optional = true }

axum/src/extract/query.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,6 @@ use serde_core::de::DeserializeOwned;
3636
///
3737
/// If the query string cannot be parsed it will reject the request with a `400
3838
/// Bad Request` response.
39-
///
40-
/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
41-
/// example.
42-
///
43-
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
44-
///
45-
/// For handling multiple values for the same query parameter, in a `?foo=1&foo=2&foo=3`
46-
/// fashion, use [`axum_extra::extract::Query`] instead.
47-
///
48-
/// [`axum_extra::extract::Query`]: https://docs.rs/axum-extra/latest/axum_extra/extract/struct.Query.html
4939
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
5040
#[derive(Debug, Clone, Copy, Default)]
5141
pub struct Query<T>(pub T);
@@ -88,7 +78,7 @@ where
8878
pub fn try_from_uri(value: &Uri) -> Result<Self, QueryRejection> {
8979
let query = value.query().unwrap_or_default();
9080
let deserializer =
91-
serde_urlencoded::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
81+
serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
9282
let params = serde_path_to_error::deserialize(deserializer)
9383
.map_err(FailedToDeserializeQueryString::from_err)?;
9484
Ok(Self(params))

axum/src/form.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ where
8484
match req.extract().await {
8585
Ok(RawForm(bytes)) => {
8686
let deserializer =
87-
serde_urlencoded::Deserializer::new(form_urlencoded::parse(&bytes));
87+
serde_html_form::Deserializer::new(form_urlencoded::parse(&bytes));
8888
let value = serde_path_to_error::deserialize(deserializer).map_err(
8989
|err| -> FormRejection {
9090
if is_get_or_head {
@@ -110,7 +110,7 @@ where
110110
{
111111
fn into_response(self) -> Response {
112112
// Extracted into separate fn so it's only compiled once for all T.
113-
fn make_response(ser_result: Result<String, serde_urlencoded::ser::Error>) -> Response {
113+
fn make_response(ser_result: Result<String, serde_html_form::ser::Error>) -> Response {
114114
match ser_result {
115115
Ok(body) => (
116116
[(CONTENT_TYPE, mime::APPLICATION_WWW_FORM_URLENCODED.as_ref())],
@@ -121,7 +121,7 @@ where
121121
}
122122
}
123123

124-
make_response(serde_urlencoded::to_string(&self.0))
124+
make_response(serde_html_form::to_string(&self.0))
125125
}
126126
}
127127
axum_core::__impl_deref!(Form);
@@ -160,7 +160,7 @@ mod tests {
160160
.uri("http://example.com/test")
161161
.method(Method::POST)
162162
.header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref())
163-
.body(Body::from(serde_urlencoded::to_string(&value).unwrap()))
163+
.body(Body::from(serde_html_form::to_string(&value).unwrap()))
164164
.unwrap();
165165
assert_eq!(Form::<T>::from_request(req, &()).await.unwrap().0, value);
166166
}
@@ -223,7 +223,7 @@ mod tests {
223223
.method(Method::POST)
224224
.header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
225225
.body(Body::from(
226-
serde_urlencoded::to_string(&Pagination {
226+
serde_html_form::to_string(&Pagination {
227227
size: Some(10),
228228
page: None,
229229
})

deny.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ skip-tree = [
2222
{ name = "windows-sys" },
2323
# pulled in by quickcheck and cookie
2424
{ name = "rand" },
25+
# duplicate dependency is intended, see axum-extra/Cargo.lock
26+
{ name = "serde_html_form" },
2527
]
2628

2729
[sources]

0 commit comments

Comments
 (0)