From 9aca158edd037fabe7352c91158e2a4889098811 Mon Sep 17 00:00:00 2001 From: Marek Kuskowski <50183564+nylonicious@users.noreply.github.com> Date: Wed, 19 Oct 2022 22:59:52 +0200 Subject: [PATCH 1/8] Add RawForm extractor --- axum/src/extract/mod.rs | 2 ++ axum/src/extract/raw_form.rs | 50 +++++++++++++++++++++++++++++++++++ axum/src/extract/rejection.rs | 15 ++++++++++- 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 axum/src/extract/raw_form.rs diff --git a/axum/src/extract/mod.rs b/axum/src/extract/mod.rs index a11921df41..8bed75b3cf 100644 --- a/axum/src/extract/mod.rs +++ b/axum/src/extract/mod.rs @@ -11,6 +11,7 @@ pub mod rejection; pub mod ws; mod host; +mod raw_form; mod raw_query; mod request_parts; mod state; @@ -26,6 +27,7 @@ pub use axum_macros::{FromRef, FromRequest, FromRequestParts}; pub use self::{ host::Host, path::Path, + raw_form::RawForm, raw_query::RawQuery, request_parts::{BodyStream, RawBody}, state::State, diff --git a/axum/src/extract/raw_form.rs b/axum/src/extract/raw_form.rs new file mode 100644 index 0000000000..76188317c4 --- /dev/null +++ b/axum/src/extract/raw_form.rs @@ -0,0 +1,50 @@ +use async_trait::async_trait; +use axum_core::extract::FromRequest; +use http::{Method, Request}; + +use super::{ + has_content_type, + rejection::{InvalidFormContentType, RawFormRejection}, +}; + +use crate::{body::HttpBody, BoxError}; + +/// Extractor that extracts the raw form string, without parsing it. +/// +/// # Example +/// ```rust,no_run +/// use axum::{ +/// extract::RawForm, +/// routing::get, +/// Router +/// }; +/// +/// async fn handler(RawForm(form): RawForm) {} +/// +/// let router = Router::new().route("/", get(handler)); +/// ``` +#[derive(Debug)] +pub struct RawForm(pub String); + +#[async_trait] +impl FromRequest for RawForm +where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, + S: Send + Sync, +{ + type Rejection = RawFormRejection; + + async fn from_request(req: Request, state: &S) -> Result { + if req.method() == Method::GET { + Ok(Self(req.uri().query().unwrap_or_default().to_owned())) + } else { + if !has_content_type(req.headers(), &mime::APPLICATION_WWW_FORM_URLENCODED) { + return Err(InvalidFormContentType.into()); + } + + Ok(Self(String::from_request(req, state).await?)) + } + } +} diff --git a/axum/src/extract/rejection.rs b/axum/src/extract/rejection.rs index 01f8d84918..f1e04e503e 100644 --- a/axum/src/extract/rejection.rs +++ b/axum/src/extract/rejection.rs @@ -59,7 +59,9 @@ define_rejection! { define_rejection! { #[status = UNSUPPORTED_MEDIA_TYPE] #[body = "Form requests must have `Content-Type: application/x-www-form-urlencoded`"] - /// Rejection type used if you try and extract the request more than once. + /// Rejection type for [`Form`](super::Form) or [`RawForm`](super::RawForm) + /// used if the `Content-Type` header is missing + /// or its value is not `application/x-www-form-urlencoded`. pub struct InvalidFormContentType; } @@ -126,6 +128,17 @@ composite_rejection! { } } +composite_rejection! { + /// Rejection used for [`RawForm`](super::RawForm). + /// + /// Contains one variant for each way the [`RawForm`](super::RawForm) extractor + /// can fail. + pub enum RawFormRejection { + InvalidFormContentType, + StringRejection, + } +} + #[cfg(feature = "json")] composite_rejection! { /// Rejection used for [`Json`](super::Json). From f483a68e52544bbfeb41827ce6a8c03e60dcb039 Mon Sep 17 00:00:00 2001 From: Marek Kuskowski <50183564+nylonicious@users.noreply.github.com> Date: Wed, 19 Oct 2022 23:21:22 +0200 Subject: [PATCH 2/8] Change RawForm(String) to RawForm(Option) --- axum/src/extract/raw_form.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/axum/src/extract/raw_form.rs b/axum/src/extract/raw_form.rs index 76188317c4..19b5a05d10 100644 --- a/axum/src/extract/raw_form.rs +++ b/axum/src/extract/raw_form.rs @@ -24,7 +24,7 @@ use crate::{body::HttpBody, BoxError}; /// let router = Router::new().route("/", get(handler)); /// ``` #[derive(Debug)] -pub struct RawForm(pub String); +pub struct RawForm(pub Option); #[async_trait] impl FromRequest for RawForm @@ -38,13 +38,13 @@ where async fn from_request(req: Request, state: &S) -> Result { if req.method() == Method::GET { - Ok(Self(req.uri().query().unwrap_or_default().to_owned())) + Ok(Self(req.uri().query().map(String::from))) } else { if !has_content_type(req.headers(), &mime::APPLICATION_WWW_FORM_URLENCODED) { return Err(InvalidFormContentType.into()); } - Ok(Self(String::from_request(req, state).await?)) + Ok(Self(Some(String::from_request(req, state).await?))) } } } From 341ad9ef39957947d893cace41fff0bb52923a47 Mon Sep 17 00:00:00 2001 From: Marek Kuskowski <50183564+nylonicious@users.noreply.github.com> Date: Wed, 19 Oct 2022 23:32:16 +0200 Subject: [PATCH 3/8] Fix tests --- .../tests/debug_handler/fail/wrong_return_type.stderr | 2 +- axum/src/extract/raw_form.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/axum-macros/tests/debug_handler/fail/wrong_return_type.stderr b/axum-macros/tests/debug_handler/fail/wrong_return_type.stderr index ce8fd05a67..4781a22cd9 100644 --- a/axum-macros/tests/debug_handler/fail/wrong_return_type.stderr +++ b/axum-macros/tests/debug_handler/fail/wrong_return_type.stderr @@ -13,7 +13,7 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied (Response<()>, T1, T2, R) (Response<()>, T1, T2, T3, R) (Response<()>, T1, T2, T3, T4, R) - and 118 others + and 119 others note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check` --> tests/debug_handler/fail/wrong_return_type.rs:4:23 | diff --git a/axum/src/extract/raw_form.rs b/axum/src/extract/raw_form.rs index 19b5a05d10..d4f2fb8823 100644 --- a/axum/src/extract/raw_form.rs +++ b/axum/src/extract/raw_form.rs @@ -12,6 +12,7 @@ use crate::{body::HttpBody, BoxError}; /// Extractor that extracts the raw form string, without parsing it. /// /// # Example +/// /// ```rust,no_run /// use axum::{ /// extract::RawForm, @@ -21,7 +22,10 @@ use crate::{body::HttpBody, BoxError}; /// /// async fn handler(RawForm(form): RawForm) {} /// -/// let router = Router::new().route("/", get(handler)); +/// let app = Router::new().route("/", get(handler)); +/// # async { +/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); +/// # }; /// ``` #[derive(Debug)] pub struct RawForm(pub Option); From 11b95daf434826d39b902cc36b1789197917b9c3 Mon Sep 17 00:00:00 2001 From: Marek Kuskowski <50183564+nylonicious@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:00:52 +0200 Subject: [PATCH 4/8] Use Bytes instead of Option and add tests --- axum/src/extract/raw_form.rs | 70 +++++++++++++++++++++++++++++++++-- axum/src/extract/rejection.rs | 2 +- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/axum/src/extract/raw_form.rs b/axum/src/extract/raw_form.rs index d4f2fb8823..06ba38d6ba 100644 --- a/axum/src/extract/raw_form.rs +++ b/axum/src/extract/raw_form.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use axum_core::extract::FromRequest; +use bytes::{Bytes, BytesMut}; use http::{Method, Request}; use super::{ @@ -9,7 +10,8 @@ use super::{ use crate::{body::HttpBody, BoxError}; -/// Extractor that extracts the raw form string, without parsing it. +/// Extractor that extracts the query bytes from the GET request or body in other methods +/// with expecting `Content-Type` header value to be `application/x-www-form-urlencoded`. /// /// # Example /// @@ -28,7 +30,7 @@ use crate::{body::HttpBody, BoxError}; /// # }; /// ``` #[derive(Debug)] -pub struct RawForm(pub Option); +pub struct RawForm(pub Bytes); #[async_trait] impl FromRequest for RawForm @@ -42,13 +44,73 @@ where async fn from_request(req: Request, state: &S) -> Result { if req.method() == Method::GET { - Ok(Self(req.uri().query().map(String::from))) + let mut bytes = BytesMut::new(); + + if let Some(query) = req.uri().query() { + bytes.extend(query.as_bytes()); + } + + Ok(Self(bytes.freeze())) } else { if !has_content_type(req.headers(), &mime::APPLICATION_WWW_FORM_URLENCODED) { return Err(InvalidFormContentType.into()); } - Ok(Self(Some(String::from_request(req, state).await?))) + Ok(Self(Bytes::from_request(req, state).await?)) } } } + +#[cfg(test)] +mod tests { + use http::{header::CONTENT_TYPE, Request}; + + use super::{InvalidFormContentType, RawForm, RawFormRejection}; + + use crate::{ + body::{Bytes, Empty, Full}, + extract::FromRequest, + }; + + async fn check_query(uri: &str, value: &[u8]) { + let req = Request::builder() + .uri(uri) + .body(Empty::::new()) + .unwrap(); + + assert_eq!(RawForm::from_request(req, &()).await.unwrap().0, value); + } + + async fn check_body(body: &'static [u8]) { + let req = Request::post("http://example.com/test") + .header(CONTENT_TYPE, mime::APPLICATION_WWW_FORM_URLENCODED.as_ref()) + .body(Full::new(Bytes::from(body))) + .unwrap(); + + assert_eq!(RawForm::from_request(req, &()).await.unwrap().0, body); + } + + #[tokio::test] + async fn test_from_query() { + check_query("http://example.com/test", b"").await; + + check_query("http://example.com/test?page=0&size=10", b"page=0&size=10").await; + } + + #[tokio::test] + async fn test_from_body() { + check_body(b"username=user&password=secure%20password").await; + } + + #[tokio::test] + async fn test_incorrect_content_type() { + let req = Request::post("http://example.com/test") + .body(Full::::from(Bytes::from("page=0&size=10"))) + .unwrap(); + + assert!(matches!( + RawForm::from_request(req, &()).await.unwrap_err(), + RawFormRejection::InvalidFormContentType(InvalidFormContentType) + )) + } +} diff --git a/axum/src/extract/rejection.rs b/axum/src/extract/rejection.rs index f1e04e503e..e49ac17fac 100644 --- a/axum/src/extract/rejection.rs +++ b/axum/src/extract/rejection.rs @@ -135,7 +135,7 @@ composite_rejection! { /// can fail. pub enum RawFormRejection { InvalidFormContentType, - StringRejection, + BytesRejection, } } From bc1745cd45dda3fa981bba0df2f90cefa3a70acf Mon Sep 17 00:00:00 2001 From: Marek Kuskowski <50183564+nylonicious@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:03:19 +0200 Subject: [PATCH 5/8] Add test for empty body --- axum/src/extract/raw_form.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/axum/src/extract/raw_form.rs b/axum/src/extract/raw_form.rs index 06ba38d6ba..5728187894 100644 --- a/axum/src/extract/raw_form.rs +++ b/axum/src/extract/raw_form.rs @@ -99,6 +99,8 @@ mod tests { #[tokio::test] async fn test_from_body() { + check_body(b"").await; + check_body(b"username=user&password=secure%20password").await; } From a3d8d66f4d0288e6cb87507fce4937c4f3c1fdb4 Mon Sep 17 00:00:00 2001 From: Marek Kuskowski <50183564+nylonicious@users.noreply.github.com> Date: Thu, 20 Oct 2022 19:29:32 +0200 Subject: [PATCH 6/8] Update CHANGELOG --- axum/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index 3401c05ccb..e6ac2ab372 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **added:** Add `#[derive(axum::extract::FromRef)]` ([#1430]) - **added:** `FromRequest` and `FromRequestParts` derive macro re-exports from [`axum-macros`] behind the `macros` feature ([#1352]) +- **added** Add `extract::RawForm` for accessing raw urlencoded query bytes or request body ([#1487]) [#1352]: https://github.com/tokio-rs/axum/pull/1352 [#1368]: https://github.com/tokio-rs/axum/pull/1368 @@ -61,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#1418]: https://github.com/tokio-rs/axum/pull/1418 [#1420]: https://github.com/tokio-rs/axum/pull/1420 [#1421]: https://github.com/tokio-rs/axum/pull/1421 +[#1487]: https://github.com/tokio-rs/axum/pull/1487 # 0.6.0-rc.2 (10. September, 2022) From 303627b9a28b2938103b622c4ba84bc231d9daa7 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 20 Oct 2022 19:52:53 +0200 Subject: [PATCH 7/8] small docs tweaks --- axum/src/extract/raw_form.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/axum/src/extract/raw_form.rs b/axum/src/extract/raw_form.rs index 5728187894..3f9f67f66a 100644 --- a/axum/src/extract/raw_form.rs +++ b/axum/src/extract/raw_form.rs @@ -10,8 +10,10 @@ use super::{ use crate::{body::HttpBody, BoxError}; -/// Extractor that extracts the query bytes from the GET request or body in other methods -/// with expecting `Content-Type` header value to be `application/x-www-form-urlencoded`. +/// Extractor that extracts raw form requests. +/// +/// For `GET` requests it will extract the raw query. For other methods it extracts the raw +/// `application/x-www-form-urlencoded` encoded request body. /// /// # Example /// From 5f276e5faae5476858424f835d556579293d6ba9 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 20 Oct 2022 19:53:07 +0200 Subject: [PATCH 8/8] changelog nit --- axum/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index e6ac2ab372..c9cf061a75 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -46,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **added:** Add `#[derive(axum::extract::FromRef)]` ([#1430]) - **added:** `FromRequest` and `FromRequestParts` derive macro re-exports from [`axum-macros`] behind the `macros` feature ([#1352]) -- **added** Add `extract::RawForm` for accessing raw urlencoded query bytes or request body ([#1487]) +- **added:** Add `extract::RawForm` for accessing raw urlencoded query bytes or request body ([#1487]) [#1352]: https://github.com/tokio-rs/axum/pull/1352 [#1368]: https://github.com/tokio-rs/axum/pull/1368