From bc8a507f583ef999bababa7f477459661b813e0d Mon Sep 17 00:00:00 2001
From: Marek Kuskowski <50183564+nylonicious@users.noreply.github.com>
Date: Thu, 20 Oct 2022 20:03:13 +0200
Subject: [PATCH] Add `RawForm` extractor (#1487)

* Add RawForm extractor

* Change RawForm(String) to RawForm(Option<String>)

* Fix tests

* Use Bytes instead of Option<String> and add tests

* Add test for empty body

* Update CHANGELOG

* small docs tweaks

* changelog nit

Co-authored-by: David Pedersen <david.pdrsn@gmail.com>
---
 .../fail/wrong_return_type.stderr             |   2 +-
 axum/CHANGELOG.md                             |   2 +
 axum/src/extract/mod.rs                       |   2 +
 axum/src/extract/raw_form.rs                  | 120 ++++++++++++++++++
 axum/src/extract/rejection.rs                 |  15 ++-
 5 files changed, 139 insertions(+), 2 deletions(-)
 create mode 100644 axum/src/extract/raw_form.rs

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 ce8fd05a..4781a22c 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/CHANGELOG.md b/axum/CHANGELOG.md
index 3401c05c..c9cf061a 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)
 
diff --git a/axum/src/extract/mod.rs b/axum/src/extract/mod.rs
index a11921df..8bed75b3 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 00000000..3f9f67f6
--- /dev/null
+++ b/axum/src/extract/raw_form.rs
@@ -0,0 +1,120 @@
+use async_trait::async_trait;
+use axum_core::extract::FromRequest;
+use bytes::{Bytes, BytesMut};
+use http::{Method, Request};
+
+use super::{
+    has_content_type,
+    rejection::{InvalidFormContentType, RawFormRejection},
+};
+
+use crate::{body::HttpBody, BoxError};
+
+/// 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
+///
+/// ```rust,no_run
+/// use axum::{
+///     extract::RawForm,
+///     routing::get,
+///     Router
+/// };
+///
+/// async fn handler(RawForm(form): RawForm) {}
+///
+/// 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 Bytes);
+
+#[async_trait]
+impl<S, B> FromRequest<S, B> for RawForm
+where
+    B: HttpBody + Send + 'static,
+    B::Data: Send,
+    B::Error: Into<BoxError>,
+    S: Send + Sync,
+{
+    type Rejection = RawFormRejection;
+
+    async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
+        if req.method() == Method::GET {
+            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(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::<Bytes>::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"").await;
+
+        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::<Bytes>::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 01f8d849..e49ac17f 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,
+        BytesRejection,
+    }
+}
+
 #[cfg(feature = "json")]
 composite_rejection! {
     /// Rejection used for [`Json`](super::Json).