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/CHANGELOG.md b/axum/CHANGELOG.md index 3401c05ccb..c9cf061a75 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 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..3f9f67f66a --- /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 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 { + 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::::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::::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 01f8d84918..e49ac17fac 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).