Skip to content

Commit

Permalink
Add RawForm extractor (#1487)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
nylonicious and davidpdrsn committed Oct 20, 2022
1 parent beb5ebb commit bc8a507
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
Expand Down
2 changes: 2 additions & 0 deletions axum/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions axum/src/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod rejection;
pub mod ws;

mod host;
mod raw_form;
mod raw_query;
mod request_parts;
mod state;
Expand All @@ -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,
Expand Down
120 changes: 120 additions & 0 deletions axum/src/extract/raw_form.rs
Original file line number Diff line number Diff line change
@@ -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)
))
}
}
15 changes: 14 additions & 1 deletion axum/src/extract/rejection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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).
Expand Down

0 comments on commit bc8a507

Please sign in to comment.