Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RawForm extractor #1487

Merged
merged 8 commits into from Oct 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
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
@@ -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?))
}
}
}
nylonicious marked this conversation as resolved.
Show resolved Hide resolved

#[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
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