From 3bd4937c346914d7365913f0a08aa8a545b3f366 Mon Sep 17 00:00:00 2001 From: Raven Date: Wed, 21 May 2025 20:18:58 +0800 Subject: [PATCH 1/2] Implement OptionalFromRequest for Form and RawForm --- axum/src/extract/raw_form.rs | 34 ++++++++++++++++++++-- axum/src/form.rs | 55 ++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/axum/src/extract/raw_form.rs b/axum/src/extract/raw_form.rs index 29cb4c6dd3..0800e184c5 100644 --- a/axum/src/extract/raw_form.rs +++ b/axum/src/extract/raw_form.rs @@ -1,6 +1,6 @@ -use axum_core::extract::{FromRequest, Request}; +use axum_core::extract::{FromRequest, OptionalFromRequest, Request}; use bytes::Bytes; -use http::Method; +use http::{header, Method}; use super::{ has_content_type, @@ -36,7 +36,7 @@ where type Rejection = RawFormRejection; async fn from_request(req: Request, state: &S) -> Result { - if req.method() == Method::GET { + if req.method() == Method::GET || req.method() == Method::HEAD { if let Some(query) = req.uri().query() { return Ok(Self(Bytes::copy_from_slice(query.as_bytes()))); } @@ -52,6 +52,34 @@ where } } +impl OptionalFromRequest for RawForm +where + S: Send + Sync, +{ + type Rejection = RawFormRejection; + + async fn from_request(req: Request, state: &S) -> Result, Self::Rejection> { + if req.method() == Method::GET || req.method() == Method::HEAD { + if let Some(query) = req.uri().query() { + return Ok(Some(Self(Bytes::copy_from_slice(query.as_bytes())))); + } + + Ok(None) + } else { + let headers = req.headers(); + if headers.get(header::CONTENT_TYPE).is_some() { + if !has_content_type(headers, &mime::APPLICATION_WWW_FORM_URLENCODED) { + return Err(InvalidFormContentType.into()); + } + + Ok(Some(Self(Bytes::from_request(req, state).await?))) + } else { + Ok(None) + } + } + } +} + #[cfg(test)] mod tests { use axum_core::body::Body; diff --git a/axum/src/form.rs b/axum/src/form.rs index dabfb65332..163ea42676 100644 --- a/axum/src/form.rs +++ b/axum/src/form.rs @@ -1,5 +1,6 @@ use crate::extract::Request; use crate::extract::{rejection::*, FromRequest, RawForm}; +use axum_core::extract::OptionalFromRequest; use axum_core::response::{IntoResponse, Response}; use axum_core::RequestExt; use http::header::CONTENT_TYPE; @@ -105,6 +106,42 @@ where } } +impl OptionalFromRequest for Form +where + T: DeserializeOwned, + S: Send + Sync, +{ + type Rejection = FormRejection; + + async fn from_request(req: Request, _state: &S) -> Result, Self::Rejection> { + let is_get_or_head = + req.method() == http::Method::GET || req.method() == http::Method::HEAD; + + match req.extract().await { + Ok(Some(RawForm(bytes))) => { + let deserializer = + serde_urlencoded::Deserializer::new(form_urlencoded::parse(&bytes)); + let value = serde_path_to_error::deserialize(deserializer).map_err( + |err| -> FormRejection { + if is_get_or_head { + FailedToDeserializeForm::from_err(err).into() + } else { + FailedToDeserializeFormBody::from_err(err).into() + } + }, + )?; + + Ok(Some(Form(value))) + } + Ok(None) => Ok(None), + Err(RawFormRejection::BytesRejection(r)) => Err(FormRejection::BytesRejection(r)), + Err(RawFormRejection::InvalidFormContentType(r)) => { + Err(FormRejection::InvalidFormContentType(r)) + } + } + } +} + impl IntoResponse for Form where T: Serialize, @@ -153,7 +190,13 @@ mod tests { .uri(uri.as_ref()) .body(Body::empty()) .unwrap(); - assert_eq!(Form::::from_request(req, &()).await.unwrap().0, value); + assert_eq!( + as FromRequest<_>>::from_request(req, &()) + .await + .unwrap() + .0, + value + ); } async fn check_body(value: T) { @@ -163,7 +206,13 @@ mod tests { .header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref()) .body(Body::from(serde_urlencoded::to_string(&value).unwrap())) .unwrap(); - assert_eq!(Form::::from_request(req, &()).await.unwrap().0, value); + assert_eq!( + as FromRequest<_>>::from_request(req, &()) + .await + .unwrap() + .0, + value + ); } #[crate::test] @@ -232,7 +281,7 @@ mod tests { )) .unwrap(); assert!(matches!( - Form::::from_request(req, &()) + as FromRequest<_>>::from_request(req, &()) .await .unwrap_err(), FormRejection::InvalidFormContentType(InvalidFormContentType) From 496183024cef59bc4959b9c7a6281a20b7ebef53 Mon Sep 17 00:00:00 2001 From: Raven Date: Wed, 21 May 2025 21:24:00 +0800 Subject: [PATCH 2/2] fix CI by remove broke ref in doc --- axum/src/docs/extract.md | 1 - 1 file changed, 1 deletion(-) diff --git a/axum/src/docs/extract.md b/axum/src/docs/extract.md index 412521728a..7da570868b 100644 --- a/axum/src/docs/extract.md +++ b/axum/src/docs/extract.md @@ -686,5 +686,4 @@ logs, enable the `tracing` feature for axum (enabled by default) and the [customize-extractor-error]: https://github.com/tokio-rs/axum/blob/main/examples/customize-extractor-error/src/main.rs [`HeaderMap`]: https://docs.rs/http/latest/http/header/struct.HeaderMap.html [`Request`]: https://docs.rs/http/latest/http/struct.Request.html -[`RequestParts::body_mut`]: crate::extract::RequestParts::body_mut [`JsonRejection::JsonDataError`]: rejection::JsonRejection::JsonDataError