Skip to content

Commit 9ef53e5

Browse files
authored
Implement OptionalFromRequest for Multipart (#3220)
1 parent 769e406 commit 9ef53e5

File tree

1 file changed

+55
-4
lines changed

1 file changed

+55
-4
lines changed

axum/src/extract/multipart.rs

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use super::{FromRequest, Request};
66
use crate::body::Bytes;
77
use axum_core::{
88
__composite_rejection as composite_rejection, __define_rejection as define_rejection,
9+
extract::OptionalFromRequest,
910
response::{IntoResponse, Response},
1011
RequestExt,
1112
};
@@ -71,13 +72,37 @@ where
7172
type Rejection = MultipartRejection;
7273

7374
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
74-
let boundary = parse_boundary(req.headers()).ok_or(InvalidBoundary)?;
75+
let boundary = content_type_str(req.headers())
76+
.and_then(|content_type| multer::parse_boundary(content_type).ok())
77+
.ok_or(InvalidBoundary)?;
7578
let stream = req.with_limited_body().into_body();
7679
let multipart = multer::Multipart::new(stream.into_data_stream(), boundary);
7780
Ok(Self { inner: multipart })
7881
}
7982
}
8083

84+
impl<S> OptionalFromRequest<S> for Multipart
85+
where
86+
S: Send + Sync,
87+
{
88+
type Rejection = MultipartRejection;
89+
90+
async fn from_request(req: Request, _state: &S) -> Result<Option<Self>, Self::Rejection> {
91+
let Some(content_type) = content_type_str(req.headers()) else {
92+
return Ok(None);
93+
};
94+
match multer::parse_boundary(content_type) {
95+
Ok(boundary) => {
96+
let stream = req.with_limited_body().into_body();
97+
let multipart = multer::Multipart::new(stream.into_data_stream(), boundary);
98+
Ok(Some(Self { inner: multipart }))
99+
}
100+
Err(multer::Error::NoMultipart) => Ok(None),
101+
Err(_) => Err(MultipartRejection::InvalidBoundary(InvalidBoundary)),
102+
}
103+
}
104+
}
105+
81106
impl Multipart {
82107
/// Yields the next [`Field`] if available.
83108
pub async fn next_field(&mut self) -> Result<Option<Field<'_>>, MultipartError> {
@@ -282,9 +307,8 @@ impl IntoResponse for MultipartError {
282307
}
283308
}
284309

285-
fn parse_boundary(headers: &HeaderMap) -> Option<String> {
286-
let content_type = headers.get(CONTENT_TYPE)?.to_str().ok()?;
287-
multer::parse_boundary(content_type).ok()
310+
fn content_type_str(headers: &HeaderMap) -> Option<&str> {
311+
headers.get(CONTENT_TYPE)?.to_str().ok()
288312
}
289313

290314
composite_rejection! {
@@ -378,4 +402,31 @@ mod tests {
378402
let res = client.post("/").multipart(form).await;
379403
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
380404
}
405+
406+
#[crate::test]
407+
async fn optional_multipart() {
408+
const BYTES: &[u8] = "<!doctype html><title>🦀</title>".as_bytes();
409+
410+
async fn handle(multipart: Option<Multipart>) -> Result<StatusCode, MultipartError> {
411+
if let Some(mut multipart) = multipart {
412+
while let Some(field) = multipart.next_field().await? {
413+
field.bytes().await?;
414+
}
415+
Ok(StatusCode::OK)
416+
} else {
417+
Ok(StatusCode::NO_CONTENT)
418+
}
419+
}
420+
421+
let app = Router::new().route("/", post(handle));
422+
let client = TestClient::new(app);
423+
let form =
424+
reqwest::multipart::Form::new().part("file", reqwest::multipart::Part::bytes(BYTES));
425+
426+
let res = client.post("/").multipart(form).await;
427+
assert_eq!(res.status(), StatusCode::OK);
428+
429+
let res = client.post("/").await;
430+
assert_eq!(res.status(), StatusCode::NO_CONTENT);
431+
}
381432
}

0 commit comments

Comments
 (0)