mirror of
https://github.com/tokio-rs/axum.git
synced 2025-03-13 19:27:53 +01:00
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>
This commit is contained in:
parent
beb5ebbe8a
commit
bc8a507f58
5 changed files with 139 additions and 2 deletions
|
@ -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
|
||||
|
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
120
axum/src/extract/raw_form.rs
Normal file
120
axum/src/extract/raw_form.rs
Normal file
|
@ -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)
|
||||
))
|
||||
}
|
||||
}
|
|
@ -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).
|
||||
|
|
Loading…
Add table
Reference in a new issue