,
+
flags: Flags,
}
diff --git a/actix-http/src/requests/request.rs b/actix-http/src/requests/request.rs
index 1750fb2f7..6a267a7a6 100644
--- a/actix-http/src/requests/request.rs
+++ b/actix-http/src/requests/request.rs
@@ -173,7 +173,7 @@ impl Request
{
/// Peer address is the directly connected peer's socket address. If a proxy is used in front of
/// the Actix Web server, then it would be address of this proxy.
///
- /// Will only return None when called in unit tests.
+ /// Will only return None when called in unit tests unless set manually.
#[inline]
pub fn peer_addr(&self) -> Option {
self.head().peer_addr
diff --git a/actix-http/src/responses/builder.rs b/actix-http/src/responses/builder.rs
index 91c69ba54..bb7d0f712 100644
--- a/actix-http/src/responses/builder.rs
+++ b/actix-http/src/responses/builder.rs
@@ -351,12 +351,9 @@ mod tests {
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain");
let resp = Response::build(StatusCode::OK)
- .content_type(mime::APPLICATION_JAVASCRIPT_UTF_8)
+ .content_type(mime::TEXT_JAVASCRIPT)
.body(Bytes::new());
- assert_eq!(
- resp.headers().get(CONTENT_TYPE).unwrap(),
- "application/javascript; charset=utf-8"
- );
+ assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/javascript");
}
#[test]
diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs
index fb38ba636..3ea88274a 100644
--- a/actix-http/src/service.rs
+++ b/actix-http/src/service.rs
@@ -241,13 +241,13 @@ where
}
/// Configuration options used when accepting TLS connection.
-#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))]
+#[cfg(feature = "__tls")]
#[derive(Debug, Default)]
pub struct TlsAcceptorConfig {
pub(crate) handshake_timeout: Option,
}
-#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))]
+#[cfg(feature = "__tls")]
impl TlsAcceptorConfig {
/// Set TLS handshake timeout duration.
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
@@ -353,12 +353,12 @@ mod openssl {
}
#[cfg(feature = "rustls-0_20")]
-mod rustls_020 {
+mod rustls_0_20 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
- rustls::{reexports::ServerConfig, Acceptor, TlsStream},
+ rustls_0_20::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
@@ -389,7 +389,7 @@ mod rustls_020 {
U::Error: fmt::Display + Into>,
U::InitError: fmt::Debug,
{
- /// Create Rustls based service.
+ /// Create Rustls v0.20 based service.
pub fn rustls(
self,
config: ServerConfig,
@@ -403,7 +403,7 @@ mod rustls_020 {
self.rustls_with_config(config, TlsAcceptorConfig::default())
}
- /// Create Rustls based service with custom TLS acceptor configuration.
+ /// Create Rustls v0.20 based service with custom TLS acceptor configuration.
pub fn rustls_with_config(
self,
mut config: ServerConfig,
@@ -449,7 +449,7 @@ mod rustls_020 {
}
#[cfg(feature = "rustls-0_21")]
-mod rustls_021 {
+mod rustls_0_21 {
use std::io;
use actix_service::ServiceFactoryExt as _;
@@ -485,7 +485,7 @@ mod rustls_021 {
U::Error: fmt::Display + Into>,
U::InitError: fmt::Debug,
{
- /// Create Rustls based service.
+ /// Create Rustls v0.21 based service.
pub fn rustls_021(
self,
config: ServerConfig,
@@ -499,7 +499,7 @@ mod rustls_021 {
self.rustls_021_with_config(config, TlsAcceptorConfig::default())
}
- /// Create Rustls based service with custom TLS acceptor configuration.
+ /// Create Rustls v0.21 based service with custom TLS acceptor configuration.
pub fn rustls_021_with_config(
self,
mut config: ServerConfig,
@@ -544,6 +544,198 @@ mod rustls_021 {
}
}
+#[cfg(feature = "rustls-0_22")]
+mod rustls_0_22 {
+ use std::io;
+
+ use actix_service::ServiceFactoryExt as _;
+ use actix_tls::accept::{
+ rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream},
+ TlsError,
+ };
+
+ use super::*;
+
+ impl HttpService, S, B, X, U>
+ where
+ S: ServiceFactory,
+ S::Future: 'static,
+ S::Error: Into> + 'static,
+ S::InitError: fmt::Debug,
+ S::Response: Into> + 'static,
+ >::Future: 'static,
+
+ B: MessageBody + 'static,
+
+ X: ServiceFactory,
+ X::Future: 'static,
+ X::Error: Into>,
+ X::InitError: fmt::Debug,
+
+ U: ServiceFactory<
+ (Request, Framed, h1::Codec>),
+ Config = (),
+ Response = (),
+ >,
+ U::Future: 'static,
+ U::Error: fmt::Display + Into>,
+ U::InitError: fmt::Debug,
+ {
+ /// Create Rustls v0.22 based service.
+ pub fn rustls_0_22(
+ self,
+ config: ServerConfig,
+ ) -> impl ServiceFactory<
+ TcpStream,
+ Config = (),
+ Response = (),
+ Error = TlsError,
+ InitError = (),
+ > {
+ self.rustls_0_22_with_config(config, TlsAcceptorConfig::default())
+ }
+
+ /// Create Rustls v0.22 based service with custom TLS acceptor configuration.
+ pub fn rustls_0_22_with_config(
+ self,
+ mut config: ServerConfig,
+ tls_acceptor_config: TlsAcceptorConfig,
+ ) -> impl ServiceFactory<
+ TcpStream,
+ Config = (),
+ Response = (),
+ Error = TlsError,
+ InitError = (),
+ > {
+ let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+ protos.extend_from_slice(&config.alpn_protocols);
+ config.alpn_protocols = protos;
+
+ let mut acceptor = Acceptor::new(config);
+
+ if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
+ acceptor.set_handshake_timeout(handshake_timeout);
+ }
+
+ acceptor
+ .map_init_err(|_| {
+ unreachable!("TLS acceptor service factory does not error on init")
+ })
+ .map_err(TlsError::into_service_error)
+ .and_then(|io: TlsStream| async {
+ let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
+ if protos.windows(2).any(|window| window == b"h2") {
+ Protocol::Http2
+ } else {
+ Protocol::Http1
+ }
+ } else {
+ Protocol::Http1
+ };
+ let peer_addr = io.get_ref().0.peer_addr().ok();
+ Ok((io, proto, peer_addr))
+ })
+ .and_then(self.map_err(TlsError::Service))
+ }
+ }
+}
+
+#[cfg(feature = "rustls-0_23")]
+mod rustls_0_23 {
+ use std::io;
+
+ use actix_service::ServiceFactoryExt as _;
+ use actix_tls::accept::{
+ rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream},
+ TlsError,
+ };
+
+ use super::*;
+
+ impl HttpService, S, B, X, U>
+ where
+ S: ServiceFactory,
+ S::Future: 'static,
+ S::Error: Into> + 'static,
+ S::InitError: fmt::Debug,
+ S::Response: Into> + 'static,
+ >::Future: 'static,
+
+ B: MessageBody + 'static,
+
+ X: ServiceFactory,
+ X::Future: 'static,
+ X::Error: Into>,
+ X::InitError: fmt::Debug,
+
+ U: ServiceFactory<
+ (Request, Framed, h1::Codec>),
+ Config = (),
+ Response = (),
+ >,
+ U::Future: 'static,
+ U::Error: fmt::Display + Into>,
+ U::InitError: fmt::Debug,
+ {
+ /// Create Rustls v0.23 based service.
+ pub fn rustls_0_23(
+ self,
+ config: ServerConfig,
+ ) -> impl ServiceFactory<
+ TcpStream,
+ Config = (),
+ Response = (),
+ Error = TlsError,
+ InitError = (),
+ > {
+ self.rustls_0_23_with_config(config, TlsAcceptorConfig::default())
+ }
+
+ /// Create Rustls v0.23 based service with custom TLS acceptor configuration.
+ pub fn rustls_0_23_with_config(
+ self,
+ mut config: ServerConfig,
+ tls_acceptor_config: TlsAcceptorConfig,
+ ) -> impl ServiceFactory<
+ TcpStream,
+ Config = (),
+ Response = (),
+ Error = TlsError,
+ InitError = (),
+ > {
+ let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+ protos.extend_from_slice(&config.alpn_protocols);
+ config.alpn_protocols = protos;
+
+ let mut acceptor = Acceptor::new(config);
+
+ if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
+ acceptor.set_handshake_timeout(handshake_timeout);
+ }
+
+ acceptor
+ .map_init_err(|_| {
+ unreachable!("TLS acceptor service factory does not error on init")
+ })
+ .map_err(TlsError::into_service_error)
+ .and_then(|io: TlsStream| async {
+ let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
+ if protos.windows(2).any(|window| window == b"h2") {
+ Protocol::Http2
+ } else {
+ Protocol::Http1
+ }
+ } else {
+ Protocol::Http1
+ };
+ let peer_addr = io.get_ref().0.peer_addr().ok();
+ Ok((io, proto, peer_addr))
+ })
+ .and_then(self.map_err(TlsError::Service))
+ }
+ }
+}
+
impl ServiceFactory<(T, Protocol, Option)>
for HttpService
where
@@ -718,7 +910,7 @@ where
handshake: Some((
crate::h2::handshake_with_timeout(io, &self.cfg),
self.cfg.clone(),
- self.flow.clone(),
+ Rc::clone(&self.flow),
conn_data,
peer_addr,
)),
@@ -734,7 +926,7 @@ where
state: State::H1 {
dispatcher: h1::Dispatcher::new(
io,
- self.flow.clone(),
+ Rc::clone(&self.flow),
self.cfg.clone(),
peer_addr,
conn_data,
diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs
index 3815e64c6..dfa9a86c9 100644
--- a/actix-http/src/test.rs
+++ b/actix-http/src/test.rs
@@ -159,8 +159,8 @@ impl TestBuffer {
#[allow(dead_code)]
pub(crate) fn clone(&self) -> Self {
Self {
- read_buf: self.read_buf.clone(),
- write_buf: self.write_buf.clone(),
+ read_buf: Rc::clone(&self.read_buf),
+ write_buf: Rc::clone(&self.write_buf),
err: self.err.clone(),
}
}
diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs
index c9fb0cde9..35b3f8e66 100644
--- a/actix-http/src/ws/frame.rs
+++ b/actix-http/src/ws/frame.rs
@@ -178,14 +178,14 @@ impl Parser {
};
if payload_len < 126 {
- dst.reserve(p_len + 2 + if mask { 4 } else { 0 });
+ dst.reserve(p_len + 2);
dst.put_slice(&[one, two | payload_len as u8]);
} else if payload_len <= 65_535 {
- dst.reserve(p_len + 4 + if mask { 4 } else { 0 });
+ dst.reserve(p_len + 4);
dst.put_slice(&[one, two | 126]);
dst.put_u16(payload_len as u16);
} else {
- dst.reserve(p_len + 10 + if mask { 4 } else { 0 });
+ dst.reserve(p_len + 10);
dst.put_slice(&[one, two | 127]);
dst.put_u64(payload_len as u64);
};
diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs
index 87f9b38f3..3ed53b70a 100644
--- a/actix-http/src/ws/mod.rs
+++ b/actix-http/src/ws/mod.rs
@@ -221,7 +221,7 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
#[cfg(test)]
mod tests {
use super::*;
- use crate::{header, test::TestRequest, Method};
+ use crate::{header, test::TestRequest};
#[test]
fn test_handshake() {
diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs
index 0653c00b0..27815eaf2 100644
--- a/actix-http/src/ws/proto.rs
+++ b/actix-http/src/ws/proto.rs
@@ -1,7 +1,4 @@
-use std::{
- convert::{From, Into},
- fmt,
-};
+use std::fmt;
use base64::prelude::*;
use tracing::error;
diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs
index cb16a4fec..4dd22b585 100644
--- a/actix-http/tests/test_openssl.rs
+++ b/actix-http/tests/test_openssl.rs
@@ -42,9 +42,11 @@ where
}
fn tls_config() -> SslAcceptor {
- let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
- let cert_file = cert.serialize_pem().unwrap();
- let key_file = cert.serialize_private_key_pem();
+ let rcgen::CertifiedKey { cert, key_pair } =
+ rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
+ let cert_file = cert.pem();
+ let key_file = key_pair.serialize_pem();
+
let cert = X509::from_pem(cert_file.as_bytes()).unwrap();
let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap();
diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs
index c94e579e5..3ca0d94c2 100644
--- a/actix-http/tests/test_rustls.rs
+++ b/actix-http/tests/test_rustls.rs
@@ -1,6 +1,6 @@
-#![cfg(feature = "rustls-0_21")]
+#![cfg(feature = "rustls-0_23")]
-extern crate tls_rustls_021 as rustls;
+extern crate tls_rustls_023 as rustls;
use std::{
convert::Infallible,
@@ -20,13 +20,13 @@ use actix_http::{
use actix_http_test::test_server;
use actix_rt::pin;
use actix_service::{fn_factory_with_config, fn_service};
-use actix_tls::connect::rustls_0_21::webpki_roots_cert_store;
+use actix_tls::connect::rustls_0_23::webpki_roots_cert_store;
use actix_utils::future::{err, ok, poll_fn};
use bytes::{Bytes, BytesMut};
use derive_more::{Display, Error};
use futures_core::{ready, Stream};
use futures_util::stream::once;
-use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName};
+use rustls::{pki_types::ServerName, ServerConfig as RustlsServerConfig};
use rustls_pemfile::{certs, pkcs8_private_keys};
async fn load_body(stream: S) -> Result
@@ -52,24 +52,25 @@ where
}
fn tls_config() -> RustlsServerConfig {
- let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
- let cert_file = cert.serialize_pem().unwrap();
- let key_file = cert.serialize_private_key_pem();
+ let rcgen::CertifiedKey { cert, key_pair } =
+ rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
+ let cert_file = cert.pem();
+ let key_file = key_pair.serialize_pem();
let cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes());
- let cert_chain = certs(cert_file)
- .unwrap()
- .into_iter()
- .map(Certificate)
- .collect();
- let mut keys = pkcs8_private_keys(key_file).unwrap();
+ let cert_chain = certs(cert_file).collect::, _>>().unwrap();
+ let mut keys = pkcs8_private_keys(key_file)
+ .collect::, _>>()
+ .unwrap();
let mut config = RustlsServerConfig::builder()
- .with_safe_defaults()
.with_no_client_auth()
- .with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
+ .with_single_cert(
+ cert_chain,
+ rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
+ )
.unwrap();
config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec());
@@ -83,7 +84,6 @@ pub fn get_negotiated_alpn_protocol(
client_alpn_protocol: &[u8],
) -> Option> {
let mut config = rustls::ClientConfig::builder()
- .with_safe_defaults()
.with_root_certificates(webpki_roots_cert_store())
.with_no_client_auth();
@@ -109,7 +109,7 @@ async fn h1() -> io::Result<()> {
let srv = test_server(move || {
HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -123,7 +123,7 @@ async fn h2() -> io::Result<()> {
let srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -141,7 +141,7 @@ async fn h1_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_11);
ok::<_, Error>(Response::ok())
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -159,7 +159,7 @@ async fn h2_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::ok())
})
- .rustls_021_with_config(
+ .rustls_0_23_with_config(
tls_config(),
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
)
@@ -180,7 +180,7 @@ async fn h2_body1() -> io::Result<()> {
let body = load_body(req.take_payload()).await?;
Ok::<_, Error>(Response::ok().set_body(body))
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -206,7 +206,7 @@ async fn h2_content_length() {
];
ok::<_, Infallible>(Response::new(statuses[indx]))
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -278,7 +278,7 @@ async fn h2_headers() {
}
ok::<_, Infallible>(config.body(data.clone()))
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -317,7 +317,7 @@ async fn h2_body2() {
let mut srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -334,7 +334,7 @@ async fn h2_head_empty() {
let mut srv = test_server(move || {
HttpService::build()
.finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -360,7 +360,7 @@ async fn h2_head_binary() {
let mut srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -385,7 +385,7 @@ async fn h2_head_binary2() {
let srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -411,7 +411,7 @@ async fn h2_body_length() {
Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
)
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -435,7 +435,7 @@ async fn h2_body_chunked_explicit() {
.body(BodyStream::new(body)),
)
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -464,7 +464,7 @@ async fn h2_response_http_error_handling() {
)
}))
}))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -494,7 +494,7 @@ async fn h2_service_error() {
let mut srv = test_server(move || {
HttpService::build()
.h2(|_| err::, _>(BadRequest))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -511,7 +511,7 @@ async fn h1_service_error() {
let mut srv = test_server(move || {
HttpService::build()
.h1(|_| err::, _>(BadRequest))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -534,7 +534,7 @@ async fn alpn_h1() -> io::Result<()> {
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(config)
+ .rustls_0_23(config)
})
.await;
@@ -556,7 +556,7 @@ async fn alpn_h2() -> io::Result<()> {
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(config)
+ .rustls_0_23(config)
})
.await;
@@ -582,7 +582,7 @@ async fn alpn_h2_1() -> io::Result<()> {
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.finish(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(config)
+ .rustls_0_23(config)
})
.await;
diff --git a/actix-multipart-derive/CHANGES.md b/actix-multipart-derive/CHANGES.md
index e36a13d04..d0c759297 100644
--- a/actix-multipart-derive/CHANGES.md
+++ b/actix-multipart-derive/CHANGES.md
@@ -2,6 +2,10 @@
## Unreleased
+## 0.7.0
+
+- Minimum supported Rust version (MSRV) is now 1.72.
+
## 0.6.1
- Update `syn` dependency to `2`.
diff --git a/actix-multipart-derive/Cargo.toml b/actix-multipart-derive/Cargo.toml
index 2f049a3fb..964ef2b74 100644
--- a/actix-multipart-derive/Cargo.toml
+++ b/actix-multipart-derive/Cargo.toml
@@ -1,13 +1,14 @@
[package]
name = "actix-multipart-derive"
-version = "0.6.1"
+version = "0.7.0"
authors = ["Jacob Halsey "]
description = "Multipart form derive macro for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"]
-homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web"
-license = "MIT OR Apache-2.0"
-edition = "2021"
+homepage.workspace = true
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
@@ -24,7 +25,10 @@ quote = "1"
syn = "2"
[dev-dependencies]
-actix-multipart = "0.6"
+actix-multipart = "0.7"
actix-web = "4"
rustversion = "1"
trybuild = "1"
+
+[lints]
+workspace = true
diff --git a/actix-multipart-derive/README.md b/actix-multipart-derive/README.md
index cd5780c56..bf75613ed 100644
--- a/actix-multipart-derive/README.md
+++ b/actix-multipart-derive/README.md
@@ -1,17 +1,16 @@
-# actix-multipart-derive
+# `actix-multipart-derive`
> The derive macro implementation for actix-multipart-derive.
+
+
[![crates.io](https://img.shields.io/crates/v/actix-multipart-derive?label=latest)](https://crates.io/crates/actix-multipart-derive)
-[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.6.1)](https://docs.rs/actix-multipart-derive/0.6.1)
-![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg)
+[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.7.0)](https://docs.rs/actix-multipart-derive/0.7.0)
+![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart-derive.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.6.1/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.6.1)
+[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.7.0/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.7.0)
[![Download](https://img.shields.io/crates/d/actix-multipart-derive.svg)](https://crates.io/crates/actix-multipart-derive)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
-## Documentation & Resources
-
-- [API Documentation](https://docs.rs/actix-multipart-derive)
-- Minimum Supported Rust Version (MSRV): 1.68
+
diff --git a/actix-multipart-derive/src/lib.rs b/actix-multipart-derive/src/lib.rs
index 9552ad2d9..6818d477c 100644
--- a/actix-multipart-derive/src/lib.rs
+++ b/actix-multipart-derive/src/lib.rs
@@ -2,8 +2,6 @@
//!
//! See [`macro@MultipartForm`] for usage examples.
-#![deny(rust_2018_idioms, nonstandard_style)]
-#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
@@ -138,7 +136,7 @@ struct ParsedField<'t> {
/// `#[multipart(duplicate_field = "")]` attribute:
///
/// - "ignore": (default) Extra fields are ignored. I.e., the first one is persisted.
-/// - "deny": A `MultipartError::UnsupportedField` error response is returned.
+/// - "deny": A `MultipartError::UnknownField` error response is returned.
/// - "replace": Each field is processed, but only the last one is persisted.
///
/// Note that `Vec` fields will ignore this option.
@@ -229,7 +227,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
// Return value when a field name is not supported by the form
let unknown_field_result = if attrs.deny_unknown_fields {
quote!(::std::result::Result::Err(
- ::actix_multipart::MultipartError::UnsupportedField(field.name().to_string())
+ ::actix_multipart::MultipartError::UnknownField(field.name().unwrap().to_string())
))
} else {
quote!(::std::result::Result::Ok(()))
@@ -292,7 +290,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
limits: &'t mut ::actix_multipart::form::Limits,
state: &'t mut ::actix_multipart::form::State,
) -> ::std::pin::Pin<::std::boxed::Box> + 't>> {
- match field.name() {
+ match field.name().unwrap() {
#handle_field_impl
_ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)),
}
diff --git a/actix-multipart-derive/tests/trybuild.rs b/actix-multipart-derive/tests/trybuild.rs
index 88aa619c6..6b25d78df 100644
--- a/actix-multipart-derive/tests/trybuild.rs
+++ b/actix-multipart-derive/tests/trybuild.rs
@@ -1,4 +1,4 @@
-#[rustversion::stable(1.68)] // MSRV
+#[rustversion::stable(1.72)] // MSRV
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();
diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md
index 50faf7cfa..c3f3b6e39 100644
--- a/actix-multipart/CHANGES.md
+++ b/actix-multipart/CHANGES.md
@@ -2,6 +2,31 @@
## Unreleased
+## 0.7.2
+
+- Fix re-exported version of `actix-multipart-derive`.
+
+## 0.7.1
+
+- Expose `LimitExceeded` error type.
+
+## 0.7.0
+
+- Add `MultipartError::ContentTypeIncompatible` variant.
+- Add `MultipartError::ContentDispositionNameMissing` variant.
+- Add `Field::bytes()` method.
+- Rename `MultipartError::{NoContentDisposition => ContentDispositionMissing}` variant.
+- Rename `MultipartError::{NoContentType => ContentTypeMissing}` variant.
+- Rename `MultipartError::{ParseContentType => ContentTypeParse}` variant.
+- Rename `MultipartError::{Boundary => BoundaryMissing}` variant.
+- Rename `MultipartError::{UnsupportedField => UnknownField}` variant.
+- Remove top-level re-exports of `test` utilities.
+
+## 0.6.2
+
+- Add testing utilities under new module `test`.
+- Minimum supported Rust version (MSRV) is now 1.72.
+
## 0.6.1
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml
index 257d56132..7a80b265f 100644
--- a/actix-multipart/Cargo.toml
+++ b/actix-multipart/Cargo.toml
@@ -1,32 +1,47 @@
[package]
name = "actix-multipart"
-version = "0.6.1"
+version = "0.7.2"
authors = [
"Nikolay Kim ",
"Jacob Halsey ",
+ "Rob Ede ",
]
-description = "Multipart form support for Actix Web"
-keywords = ["http", "web", "framework", "async", "futures"]
-homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web"
-license = "MIT OR Apache-2.0"
-edition = "2021"
+description = "Multipart request & form support for Actix Web"
+keywords = ["http", "actix", "web", "multipart", "form"]
+homepage.workspace = true
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
+[package.metadata.cargo_check_external_types]
+allowed_external_types = [
+ "actix_http::*",
+ "actix_multipart_derive::*",
+ "actix_utils::*",
+ "actix_web::*",
+ "bytes::*",
+ "futures_core::*",
+ "mime::*",
+ "serde_json::*",
+ "serde_plain::*",
+ "serde::*",
+ "tempfile::*",
+]
+
[features]
default = ["tempfile", "derive"]
derive = ["actix-multipart-derive"]
tempfile = ["dep:tempfile", "tokio/fs"]
[dependencies]
-actix-multipart-derive = { version = "=0.6.1", optional = true }
+actix-multipart-derive = { version = "=0.7.0", optional = true }
actix-utils = "3"
actix-web = { version = "4", default-features = false }
-bytes = "1"
derive_more = "0.99.5"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
@@ -35,6 +50,7 @@ local-waker = "0.1"
log = "0.4"
memchr = "2.5"
mime = "0.3"
+rand = "0.8"
serde = "1"
serde_json = "1"
serde_plain = "1"
@@ -46,7 +62,15 @@ actix-http = "3"
actix-multipart-rfc7578 = "0.10"
actix-rt = "2.2"
actix-test = "0.1"
+actix-web = "4"
+assert_matches = "1"
awc = "3"
+env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
+futures-test = "0.3"
+multer = "3"
tokio = { version = "1.24.2", features = ["sync"] }
tokio-stream = "0.1"
+
+[lints]
+workspace = true
diff --git a/actix-multipart/README.md b/actix-multipart/README.md
index 8fe0328ab..ec2e94bd8 100644
--- a/actix-multipart/README.md
+++ b/actix-multipart/README.md
@@ -1,17 +1,74 @@
-# actix-multipart
+# `actix-multipart`
-> Multipart form support for Actix Web.
+
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
-[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.6.1)](https://docs.rs/actix-multipart/0.6.1)
-![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg)
+[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.7.2)](https://docs.rs/actix-multipart/0.7.2)
+![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.6.1/status.svg)](https://deps.rs/crate/actix-multipart/0.6.1)
+[![dependency status](https://deps.rs/crate/actix-multipart/0.7.2/status.svg)](https://deps.rs/crate/actix-multipart/0.7.2)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
-## Documentation & Resources
+
-- [API Documentation](https://docs.rs/actix-multipart)
-- Minimum Supported Rust Version (MSRV): 1.68
+
+
+Multipart request & form support for Actix Web.
+
+The [`Multipart`] extractor aims to support all kinds of `multipart/*` requests, including `multipart/form-data`, `multipart/related` and `multipart/mixed`. This is a lower-level extractor which supports reading [multipart fields](Field), in the order they are sent by the client.
+
+Due to additional requirements for `multipart/form-data` requests, the higher level [`MultipartForm`] extractor and derive macro only supports this media type.
+
+## Examples
+
+```rust
+use actix_web::{post, App, HttpServer, Responder};
+
+use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+struct Metadata {
+ name: String,
+}
+
+#[derive(Debug, MultipartForm)]
+struct UploadForm {
+ #[multipart(limit = "100MB")]
+ file: TempFile,
+ json: MpJson,
+}
+
+#[post("/videos")]
+pub async fn post_video(MultipartForm(form): MultipartForm) -> impl Responder {
+ format!(
+ "Uploaded file {}, with size: {}",
+ form.json.name, form.file.size
+ )
+}
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ HttpServer::new(move || App::new().service(post_video))
+ .bind(("127.0.0.1", 8080))?
+ .run()
+ .await
+}
+```
+
+cURL request:
+
+```sh
+curl -v --request POST \
+ --url http://localhost:8080/videos \
+ -F 'json={"name": "Cargo.lock"};type=application/json' \
+ -F file=@./Cargo.lock
+```
+
+[`MultipartForm`]: struct@form::MultipartForm
+
+
+
+[More available in the examples repo →](https://github.com/actix/examples/tree/master/forms/multipart)
diff --git a/actix-multipart/examples/form.rs b/actix-multipart/examples/form.rs
new file mode 100644
index 000000000..a90aeff96
--- /dev/null
+++ b/actix-multipart/examples/form.rs
@@ -0,0 +1,36 @@
+use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
+use actix_web::{middleware::Logger, post, App, HttpServer, Responder};
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+struct Metadata {
+ name: String,
+}
+
+#[derive(Debug, MultipartForm)]
+struct UploadForm {
+ #[multipart(limit = "100MB")]
+ file: TempFile,
+ json: MpJson,
+}
+
+#[post("/videos")]
+async fn post_video(MultipartForm(form): MultipartForm) -> impl Responder {
+ format!(
+ "Uploaded file {}, with size: {}\ntemporary file ({}) was deleted\n",
+ form.json.name,
+ form.file.size,
+ form.file.file.path().display(),
+ )
+}
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
+
+ HttpServer::new(move || App::new().service(post_video).wrap(Logger::default()))
+ .workers(2)
+ .bind(("127.0.0.1", 8080))?
+ .run()
+ .await
+}
diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs
index 77b5a559f..cdb608738 100644
--- a/actix-multipart/src/error.rs
+++ b/actix-multipart/src/error.rs
@@ -10,78 +10,96 @@ use derive_more::{Display, Error, From};
/// A set of errors that can occur during parsing multipart streams.
#[derive(Debug, Display, From, Error)]
#[non_exhaustive]
-pub enum MultipartError {
- /// Content-Disposition header is not found or is not equal to "form-data".
+pub enum Error {
+ /// Could not find Content-Type header.
+ #[display(fmt = "Could not find Content-Type header")]
+ ContentTypeMissing,
+
+ /// Could not parse Content-Type header.
+ #[display(fmt = "Could not parse Content-Type header")]
+ ContentTypeParse,
+
+ /// Parsed Content-Type did not have "multipart" top-level media type.
///
- /// According to [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) a
- /// Content-Disposition header must always be present and equal to "form-data".
- #[display(fmt = "No Content-Disposition `form-data` header")]
- NoContentDisposition,
+ /// Also raised when extracting a [`MultipartForm`] from a request that does not have the
+ /// "multipart/form-data" media type.
+ ///
+ /// [`MultipartForm`]: struct@crate::form::MultipartForm
+ #[display(fmt = "Parsed Content-Type did not have "multipart" top-level media type")]
+ ContentTypeIncompatible,
- /// Content-Type header is not found
- #[display(fmt = "No Content-Type header found")]
- NoContentType,
-
- /// Can not parse Content-Type header
- #[display(fmt = "Can not parse Content-Type header")]
- ParseContentType,
-
- /// Multipart boundary is not found
+ /// Multipart boundary is not found.
#[display(fmt = "Multipart boundary is not found")]
- Boundary,
+ BoundaryMissing,
- /// Nested multipart is not supported
+ /// Content-Disposition header was not found or not of disposition type "form-data" when parsing
+ /// a "form-data" field.
+ ///
+ /// As per [RFC 7578 §4.2], a "multipart/form-data" field's Content-Disposition header must
+ /// always be present and have a disposition type of "form-data".
+ ///
+ /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
+ #[display(fmt = "Content-Disposition header was not found when parsing a \"form-data\" field")]
+ ContentDispositionMissing,
+
+ /// Content-Disposition name parameter was not found when parsing a "form-data" field.
+ ///
+ /// As per [RFC 7578 §4.2], a "multipart/form-data" field's Content-Disposition header must
+ /// always include a "name" parameter.
+ ///
+ /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
+ #[display(fmt = "Content-Disposition header was not found when parsing a \"form-data\" field")]
+ ContentDispositionNameMissing,
+
+ /// Nested multipart is not supported.
#[display(fmt = "Nested multipart is not supported")]
Nested,
- /// Multipart stream is incomplete
+ /// Multipart stream is incomplete.
#[display(fmt = "Multipart stream is incomplete")]
Incomplete,
- /// Error during field parsing
- #[display(fmt = "{}", _0)]
+ /// Field parsing failed.
+ #[display(fmt = "Error during field parsing")]
Parse(ParseError),
- /// Payload error
- #[display(fmt = "{}", _0)]
+ /// HTTP payload error.
+ #[display(fmt = "Payload error")]
Payload(PayloadError),
- /// Not consumed
- #[display(fmt = "Multipart stream is not consumed")]
+ /// Stream is not consumed.
+ #[display(fmt = "Stream is not consumed")]
NotConsumed,
- /// An error from a field handler in a form
- #[display(
- fmt = "An error occurred processing field `{}`: {}",
- field_name,
- source
- )]
+ /// Form field handler raised error.
+ #[display(fmt = "An error occurred processing field: {name}")]
Field {
- field_name: String,
+ name: String,
source: actix_web::Error,
},
- /// Duplicate field
- #[display(fmt = "Duplicate field found for: `{}`", _0)]
+ /// Duplicate field found (for structure that opted-in to denying duplicate fields).
+ #[display(fmt = "Duplicate field found: {_0}")]
#[from(ignore)]
DuplicateField(#[error(not(source))] String),
- /// Missing field
- #[display(fmt = "Field with name `{}` is required", _0)]
+ /// Required field is missing.
+ #[display(fmt = "Required field is missing: {_0}")]
#[from(ignore)]
MissingField(#[error(not(source))] String),
- /// Unknown field
- #[display(fmt = "Unsupported field `{}`", _0)]
+ /// Unknown field (for structure that opted-in to denying unknown fields).
+ #[display(fmt = "Unknown field: {_0}")]
#[from(ignore)]
- UnsupportedField(#[error(not(source))] String),
+ UnknownField(#[error(not(source))] String),
}
-/// Return `BadRequest` for `MultipartError`
-impl ResponseError for MultipartError {
+/// Return `BadRequest` for `MultipartError`.
+impl ResponseError for Error {
fn status_code(&self) -> StatusCode {
match &self {
- MultipartError::Field { source, .. } => source.as_response_error().status_code(),
+ Error::Field { source, .. } => source.as_response_error().status_code(),
+ Error::ContentTypeIncompatible => StatusCode::UNSUPPORTED_MEDIA_TYPE,
_ => StatusCode::BAD_REQUEST,
}
}
@@ -93,7 +111,7 @@ mod tests {
#[test]
fn test_multipart_error() {
- let resp = MultipartError::Boundary.error_response();
+ let resp = Error::BoundaryMissing.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
}
diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs
index 56ed69ae4..31999228e 100644
--- a/actix-multipart/src/extractor.rs
+++ b/actix-multipart/src/extractor.rs
@@ -1,21 +1,20 @@
-//! Multipart payload support
-
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
-use crate::server::Multipart;
+use crate::multipart::Multipart;
-/// Get request's payload as multipart stream.
+/// Extract request's payload as multipart stream.
///
-/// Content-type: multipart/form-data;
+/// Content-type: multipart/*;
///
/// # Examples
+///
/// ```
-/// use actix_web::{web, HttpResponse, Error};
+/// use actix_web::{web, HttpResponse};
/// use actix_multipart::Multipart;
/// use futures_util::StreamExt as _;
///
-/// async fn index(mut payload: Multipart) -> Result {
+/// async fn index(mut payload: Multipart) -> actix_web::Result {
/// // iterate over multipart stream
/// while let Some(item) = payload.next().await {
/// let mut field = item?;
@@ -26,7 +25,7 @@ use crate::server::Multipart;
/// }
/// }
///
-/// Ok(HttpResponse::Ok().into())
+/// Ok(HttpResponse::Ok().finish())
/// }
/// ```
impl FromRequest for Multipart {
@@ -35,9 +34,6 @@ impl FromRequest for Multipart {
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
- ready(Ok(match Multipart::boundary(req.headers()) {
- Ok(boundary) => Multipart::from_boundary(boundary, payload.take()),
- Err(err) => Multipart::from_error(err),
- }))
+ ready(Ok(Multipart::from_req(req, payload)))
}
}
diff --git a/actix-multipart/src/field.rs b/actix-multipart/src/field.rs
new file mode 100644
index 000000000..f4eb601fb
--- /dev/null
+++ b/actix-multipart/src/field.rs
@@ -0,0 +1,501 @@
+use std::{
+ cell::RefCell,
+ cmp, fmt,
+ future::poll_fn,
+ mem,
+ pin::Pin,
+ rc::Rc,
+ task::{ready, Context, Poll},
+};
+
+use actix_web::{
+ error::PayloadError,
+ http::header::{self, ContentDisposition, HeaderMap},
+ web::{Bytes, BytesMut},
+};
+use derive_more::{Display, Error};
+use futures_core::Stream;
+use mime::Mime;
+
+use crate::{
+ error::Error,
+ payload::{PayloadBuffer, PayloadRef},
+ safety::Safety,
+};
+
+/// Error type returned from [`Field::bytes()`] when field data is larger than limit.
+#[derive(Debug, Display, Error)]
+#[display(fmt = "size limit exceeded while collecting field data")]
+#[non_exhaustive]
+pub struct LimitExceeded;
+
+/// A single field in a multipart stream.
+pub struct Field {
+ /// Field's Content-Type.
+ content_type: Option,
+
+ /// Field's Content-Disposition.
+ content_disposition: Option,
+
+ /// Form field name.
+ ///
+ /// A non-optional storage for form field names to avoid unwraps in `form` module. Will be an
+ /// empty string in non-form contexts.
+ ///
+ // INVARIANT: always non-empty when request content-type is multipart/form-data.
+ pub(crate) form_field_name: String,
+
+ /// Field's header map.
+ headers: HeaderMap,
+
+ safety: Safety,
+ inner: Rc>,
+}
+
+impl Field {
+ pub(crate) fn new(
+ content_type: Option,
+ content_disposition: Option,
+ form_field_name: Option,
+ headers: HeaderMap,
+ safety: Safety,
+ inner: Rc>,
+ ) -> Self {
+ Field {
+ content_type,
+ content_disposition,
+ form_field_name: form_field_name.unwrap_or_default(),
+ headers,
+ inner,
+ safety,
+ }
+ }
+
+ /// Returns a reference to the field's header map.
+ pub fn headers(&self) -> &HeaderMap {
+ &self.headers
+ }
+
+ /// Returns a reference to the field's content (mime) type, if it is supplied by the client.
+ ///
+ /// According to [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578#section-4.4), if it is not
+ /// present, it should default to "text/plain". Note it is the responsibility of the client to
+ /// provide the appropriate content type, there is no attempt to validate this by the server.
+ pub fn content_type(&self) -> Option<&Mime> {
+ self.content_type.as_ref()
+ }
+
+ /// Returns this field's parsed Content-Disposition header, if set.
+ ///
+ /// # Validation
+ ///
+ /// Per [RFC 7578 §4.2], the parts of a multipart/form-data payload MUST contain a
+ /// Content-Disposition header field where the disposition type is `form-data` and MUST also
+ /// contain an additional parameter of `name` with its value being the original field name from
+ /// the form. This requirement is enforced during extraction for multipart/form-data requests,
+ /// but not other kinds of multipart requests (such as multipart/related).
+ ///
+ /// As such, it is safe to `.unwrap()` calls `.content_disposition()` if you've verified.
+ ///
+ /// The [`name()`](Self::name) method is also provided as a convenience for obtaining the
+ /// aforementioned name parameter.
+ ///
+ /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
+ pub fn content_disposition(&self) -> Option<&ContentDisposition> {
+ self.content_disposition.as_ref()
+ }
+
+ /// Returns the field's name, if set.
+ ///
+ /// See [`content_disposition()`](Self::content_disposition) regarding guarantees on presence of
+ /// the "name" field.
+ pub fn name(&self) -> Option<&str> {
+ self.content_disposition()?.get_name()
+ }
+
+ /// Collects the raw field data, up to `limit` bytes.
+ ///
+ /// # Errors
+ ///
+ /// Any errors produced by the data stream are returned as `Ok(Err(Error))` immediately.
+ ///
+ /// If the buffered data size would exceed `limit`, an `Err(LimitExceeded)` is returned. Note
+ /// that, in this case, the full data stream is exhausted before returning the error so that
+ /// subsequent fields can still be read. To better defend against malicious/infinite requests,
+ /// it is advisable to also put a timeout on this call.
+ pub async fn bytes(&mut self, limit: usize) -> Result, LimitExceeded> {
+ /// Sensible default (2kB) for initial, bounded allocation when collecting body bytes.
+ const INITIAL_ALLOC_BYTES: usize = 2 * 1024;
+
+ let mut exceeded_limit = false;
+ let mut buf = BytesMut::with_capacity(INITIAL_ALLOC_BYTES);
+
+ let mut field = Pin::new(self);
+
+ match poll_fn(|cx| loop {
+ match ready!(field.as_mut().poll_next(cx)) {
+ // if already over limit, discard chunk to advance multipart request
+ Some(Ok(_chunk)) if exceeded_limit => {}
+
+ // if limit is exceeded set flag to true and continue
+ Some(Ok(chunk)) if buf.len() + chunk.len() > limit => {
+ exceeded_limit = true;
+ // eagerly de-allocate field data buffer
+ let _ = mem::take(&mut buf);
+ }
+
+ Some(Ok(chunk)) => buf.extend_from_slice(&chunk),
+
+ None => return Poll::Ready(Ok(())),
+ Some(Err(err)) => return Poll::Ready(Err(err)),
+ }
+ })
+ .await
+ {
+ // propagate error returned from body poll
+ Err(err) => Ok(Err(err)),
+
+ // limit was exceeded while reading body
+ Ok(()) if exceeded_limit => Err(LimitExceeded),
+
+ // otherwise return body buffer
+ Ok(()) => Ok(Ok(buf.freeze())),
+ }
+ }
+}
+
+impl Stream for Field {
+ type Item = Result;
+
+ fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll