,
+
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/service.rs b/actix-http/src/service.rs
index fb38ba636..a58be93c7 100644
--- a/actix-http/src/service.rs
+++ b/actix-http/src/service.rs
@@ -241,13 +241,25 @@ where
}
/// Configuration options used when accepting TLS connection.
-#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))]
+#[cfg(any(
+ feature = "openssl",
+ feature = "rustls-0_20",
+ feature = "rustls-0_21",
+ feature = "rustls-0_22",
+ feature = "rustls-0_23",
+))]
#[derive(Debug, Default)]
pub struct TlsAcceptorConfig {
pub(crate) handshake_timeout: Option,
}
-#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))]
+#[cfg(any(
+ feature = "openssl",
+ feature = "rustls-0_20",
+ feature = "rustls-0_21",
+ feature = "rustls-0_22",
+ feature = "rustls-0_23",
+))]
impl TlsAcceptorConfig {
/// Set TLS handshake timeout duration.
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
@@ -353,12 +365,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 +401,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 +415,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 +461,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 +497,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 +511,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 +556,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
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 b4d8ed1a5..4dd22b585 100644
--- a/actix-http/tests/test_openssl.rs
+++ b/actix-http/tests/test_openssl.rs
@@ -1,5 +1,4 @@
#![cfg(feature = "openssl")]
-#![allow(clippy::uninlined_format_args)]
extern crate tls_openssl as openssl;
@@ -43,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-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs
index a2866613b..9a78074c4 100644
--- a/actix-http/tests/test_ws.rs
+++ b/actix-http/tests/test_ws.rs
@@ -1,5 +1,3 @@
-#![allow(clippy::uninlined_format_args)]
-
use std::{
cell::Cell,
convert::Infallible,
diff --git a/actix-multipart-derive/CHANGES.md b/actix-multipart-derive/CHANGES.md
index e36a13d04..1b44ba4b7 100644
--- a/actix-multipart-derive/CHANGES.md
+++ b/actix-multipart-derive/CHANGES.md
@@ -2,6 +2,8 @@
## Unreleased
+- 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 75b4c723b..e978864a3 100644
--- a/actix-multipart-derive/Cargo.toml
+++ b/actix-multipart-derive/Cargo.toml
@@ -4,10 +4,11 @@ version = "0.6.1"
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.git"
-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"]
diff --git a/actix-multipart-derive/README.md b/actix-multipart-derive/README.md
index 2737410f6..ec0afffdd 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.
+
+
[](https://crates.io/crates/actix-multipart-derive)
-[](https://docs.rs/actix-multipart-derive/0.5.0)
-
+[](https://docs.rs/actix-multipart-derive/0.6.1)
+

-[](https://deps.rs/crate/actix-multipart-derive/0.5.0)
+[](https://deps.rs/crate/actix-multipart-derive/0.6.1)
[](https://crates.io/crates/actix-multipart-derive)
[](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/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..a91edf9c8 100644
--- a/actix-multipart/CHANGES.md
+++ b/actix-multipart/CHANGES.md
@@ -2,6 +2,11 @@
## Unreleased
+## 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 661d3812b..5e9b78d84 100644
--- a/actix-multipart/Cargo.toml
+++ b/actix-multipart/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-multipart"
-version = "0.6.1"
+version = "0.6.2"
authors = [
"Nikolay Kim ",
"Jacob Halsey ",
@@ -8,7 +8,7 @@ authors = [
description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web.git"
+repository = "https://github.com/actix/actix-web"
license = "MIT OR Apache-2.0"
edition = "2021"
@@ -50,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"
@@ -61,7 +62,9 @@ actix-http = "3"
actix-multipart-rfc7578 = "0.10"
actix-rt = "2.2"
actix-test = "0.1"
+actix-web = "4"
awc = "3"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
+multer = "3"
tokio = { version = "1.24.2", features = ["sync"] }
tokio-stream = "0.1"
diff --git a/actix-multipart/README.md b/actix-multipart/README.md
index 8fe0328ab..c7697785a 100644
--- a/actix-multipart/README.md
+++ b/actix-multipart/README.md
@@ -1,17 +1,77 @@
-# actix-multipart
+# `actix-multipart`
> Multipart form support for Actix Web.
+
+
[](https://crates.io/crates/actix-multipart)
-[](https://docs.rs/actix-multipart/0.6.1)
-
+[](https://docs.rs/actix-multipart/0.6.2)
+

-[](https://deps.rs/crate/actix-multipart/0.6.1)
+[](https://deps.rs/crate/actix-multipart/0.6.2)
[](https://crates.io/crates/actix-multipart)
[](https://discord.gg/NWpN5mmg3x)
-## Documentation & Resources
+
-- [API Documentation](https://docs.rs/actix-multipart)
-- Minimum Supported Rust Version (MSRV): 1.68
+## Example
+
+Dependencies:
+
+```toml
+[dependencies]
+actix-multipart = "0.6"
+actix-web = "4.5"
+serde = { version = "1.0", features = ["derive"] }
+```
+
+Code:
+
+```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 :
+
+```bash
+curl -v --request POST \
+ --url http://localhost:8080/videos \
+ -F 'json={"name": "Cargo.lock"};type=application/json' \
+ -F file=@./Cargo.lock
+```
+
+### Examples
+
+https://github.com/actix/examples/tree/master/forms/multipart
diff --git a/actix-multipart/src/form/json.rs b/actix-multipart/src/form/json.rs
index fb90a82b9..bb4e03bf6 100644
--- a/actix-multipart/src/form/json.rs
+++ b/actix-multipart/src/form/json.rs
@@ -131,14 +131,13 @@ impl Default for JsonConfig {
#[cfg(test)]
mod tests {
- use std::{collections::HashMap, io::Cursor};
+ use std::collections::HashMap;
- use actix_multipart_rfc7578::client::multipart;
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
+ use bytes::Bytes;
use crate::form::{
json::{Json, JsonConfig},
- tests::send_form,
MultipartForm,
};
@@ -155,6 +154,8 @@ mod tests {
HttpResponse::Ok().finish()
}
+ const TEST_JSON: &str = r#"{"key1": "value1", "key2": "value2"}"#;
+
#[actix_rt::test]
async fn test_json_without_content_type() {
let srv = actix_test::start(|| {
@@ -163,10 +164,16 @@ mod tests {
.app_data(JsonConfig::default().validate_content_type(false))
});
- let mut form = multipart::Form::default();
- form.add_text("json", "{\"key1\": \"value1\", \"key2\": \"value2\"}");
- let response = send_form(&srv, form, "/").await;
- assert_eq!(response.status(), StatusCode::OK);
+ let (body, headers) = crate::test::create_form_data_payload_and_headers(
+ "json",
+ None,
+ None,
+ Bytes::from_static(TEST_JSON.as_bytes()),
+ );
+ let mut req = srv.post("/");
+ *req.headers_mut() = headers;
+ let res = req.send_body(body).await.unwrap();
+ assert_eq!(res.status(), StatusCode::OK);
}
#[actix_rt::test]
@@ -178,17 +185,27 @@ mod tests {
});
// Deny because wrong content type
- let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
- let mut form = multipart::Form::default();
- form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_OCTET_STREAM);
- let response = send_form(&srv, form, "/").await;
- assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+ let (body, headers) = crate::test::create_form_data_payload_and_headers(
+ "json",
+ None,
+ Some(mime::APPLICATION_OCTET_STREAM),
+ Bytes::from_static(TEST_JSON.as_bytes()),
+ );
+ let mut req = srv.post("/");
+ *req.headers_mut() = headers;
+ let res = req.send_body(body).await.unwrap();
+ assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Allow because correct content type
- let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
- let mut form = multipart::Form::default();
- form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_JSON);
- let response = send_form(&srv, form, "/").await;
- assert_eq!(response.status(), StatusCode::OK);
+ let (body, headers) = crate::test::create_form_data_payload_and_headers(
+ "json",
+ None,
+ Some(mime::APPLICATION_JSON),
+ Bytes::from_static(TEST_JSON.as_bytes()),
+ );
+ let mut req = srv.post("/");
+ *req.headers_mut() = headers;
+ let res = req.send_body(body).await.unwrap();
+ assert_eq!(res.status(), StatusCode::OK);
}
}
diff --git a/actix-multipart/src/form/mod.rs b/actix-multipart/src/form/mod.rs
index 67adfd4b2..451b103fd 100644
--- a/actix-multipart/src/form/mod.rs
+++ b/actix-multipart/src/form/mod.rs
@@ -313,7 +313,8 @@ where
let entry = field_limits
.entry(field.name().to_owned())
.or_insert_with(|| T::limit(field.name()));
- limits.field_limit_remaining = entry.to_owned();
+
+ limits.field_limit_remaining.clone_from(entry);
T::handle_field(&req, field, &mut limits, &mut state).await?;
diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs
index 615a8e6de..d19e951e6 100644
--- a/actix-multipart/src/lib.rs
+++ b/actix-multipart/src/lib.rs
@@ -1,8 +1,43 @@
//! Multipart form support for Actix Web.
+//! # Examples
+//! ```no_run
+//! 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
+//! }
+//! ```
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
-#![allow(clippy::borrow_interior_mutable_const, clippy::uninlined_format_args)]
+#![allow(clippy::borrow_interior_mutable_const)]
#![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))]
@@ -13,11 +48,14 @@ extern crate self as actix_multipart;
mod error;
mod extractor;
-mod server;
-
pub mod form;
+mod server;
+pub mod test;
pub use self::{
error::MultipartError,
server::{Field, Multipart},
+ test::{
+ create_form_data_payload_and_headers, create_form_data_payload_and_headers_with_boundary,
+ },
};
diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs
index c08031eba..d0f833318 100644
--- a/actix-multipart/src/server.rs
+++ b/actix-multipart/src/server.rs
@@ -863,13 +863,15 @@ mod tests {
test::TestRequest,
FromRequest,
};
- use bytes::Bytes;
+ use bytes::BufMut as _;
use futures_util::{future::lazy, StreamExt as _};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use super::*;
+ const BOUNDARY: &str = "abbc761f78ff4d7cb7573b5a23f96ef0";
+
#[actix_rt::test]
async fn test_boundary() {
let headers = HeaderMap::new();
@@ -965,6 +967,26 @@ mod tests {
}
fn create_simple_request_with_header() -> (Bytes, HeaderMap) {
+ let (body, headers) = crate::test::create_form_data_payload_and_headers_with_boundary(
+ BOUNDARY,
+ "file",
+ Some("fn.txt".to_owned()),
+ Some(mime::TEXT_PLAIN_UTF_8),
+ Bytes::from_static(b"data"),
+ );
+
+ let mut buf = BytesMut::with_capacity(body.len() + 14);
+
+ // add junk before form to test pre-boundary data rejection
+ buf.put("testasdadsad\r\n".as_bytes());
+
+ buf.put(body);
+
+ (buf.freeze(), headers)
+ }
+
+ // TODO: use test utility when multi-file support is introduced
+ fn create_double_request_with_header() -> (Bytes, HeaderMap) {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
@@ -990,7 +1012,7 @@ mod tests {
#[actix_rt::test]
async fn test_multipart_no_end_crlf() {
let (sender, payload) = create_stream();
- let (mut bytes, headers) = create_simple_request_with_header();
+ let (mut bytes, headers) = create_double_request_with_header();
let bytes_stripped = bytes.split_to(bytes.len()); // strip crlf
sender.send(Ok(bytes_stripped)).unwrap();
@@ -1017,7 +1039,7 @@ mod tests {
#[actix_rt::test]
async fn test_multipart() {
let (sender, payload) = create_stream();
- let (bytes, headers) = create_simple_request_with_header();
+ let (bytes, headers) = create_double_request_with_header();
sender.send(Ok(bytes)).unwrap();
@@ -1080,7 +1102,7 @@ mod tests {
#[actix_rt::test]
async fn test_stream() {
- let (bytes, headers) = create_simple_request_with_header();
+ let (bytes, headers) = create_double_request_with_header();
let payload = SlowStream::new(bytes);
let mut multipart = Multipart::new(&headers, payload);
@@ -1319,7 +1341,7 @@ mod tests {
#[actix_rt::test]
async fn test_drop_field_awaken_multipart() {
let (sender, payload) = create_stream();
- let (bytes, headers) = create_simple_request_with_header();
+ let (bytes, headers) = create_double_request_with_header();
sender.send(Ok(bytes)).unwrap();
drop(sender); // eof
diff --git a/actix-multipart/src/test.rs b/actix-multipart/src/test.rs
new file mode 100644
index 000000000..77d918283
--- /dev/null
+++ b/actix-multipart/src/test.rs
@@ -0,0 +1,217 @@
+use actix_web::http::header::{self, HeaderMap};
+use bytes::{BufMut as _, Bytes, BytesMut};
+use mime::Mime;
+use rand::{
+ distributions::{Alphanumeric, DistString as _},
+ thread_rng,
+};
+
+const CRLF: &[u8] = b"\r\n";
+const CRLF_CRLF: &[u8] = b"\r\n\r\n";
+const HYPHENS: &[u8] = b"--";
+const BOUNDARY_PREFIX: &str = "------------------------";
+
+/// Constructs a `multipart/form-data` payload from bytes and metadata.
+///
+/// Returned header map can be extended or merged with existing headers.
+///
+/// Multipart boundary used is a random alphanumeric string.
+///
+/// # Examples
+///
+/// ```
+/// use actix_multipart::test::create_form_data_payload_and_headers;
+/// use actix_web::test::TestRequest;
+/// use bytes::Bytes;
+/// use memchr::memmem::find;
+///
+/// let (body, headers) = create_form_data_payload_and_headers(
+/// "foo",
+/// Some("lorem.txt".to_owned()),
+/// Some(mime::TEXT_PLAIN_UTF_8),
+/// Bytes::from_static(b"Lorem ipsum."),
+/// );
+///
+/// assert!(find(&body, b"foo").is_some());
+/// assert!(find(&body, b"lorem.txt").is_some());
+/// assert!(find(&body, b"text/plain; charset=utf-8").is_some());
+/// assert!(find(&body, b"Lorem ipsum.").is_some());
+///
+/// let req = TestRequest::default();
+///
+/// // merge header map into existing test request and set multipart body
+/// let req = headers
+/// .into_iter()
+/// .fold(req, |req, hdr| req.insert_header(hdr))
+/// .set_payload(body)
+/// .to_http_request();
+///
+/// assert!(
+/// req.headers()
+/// .get("content-type")
+/// .unwrap()
+/// .to_str()
+/// .unwrap()
+/// .starts_with("multipart/form-data; boundary=\"")
+/// );
+/// ```
+pub fn create_form_data_payload_and_headers(
+ name: &str,
+ filename: Option,
+ content_type: Option,
+ file: Bytes,
+) -> (Bytes, HeaderMap) {
+ let boundary = Alphanumeric.sample_string(&mut thread_rng(), 32);
+
+ create_form_data_payload_and_headers_with_boundary(
+ &boundary,
+ name,
+ filename,
+ content_type,
+ file,
+ )
+}
+
+/// Constructs a `multipart/form-data` payload from bytes and metadata with a fixed boundary.
+///
+/// See [`create_form_data_payload_and_headers`] for more details.
+pub fn create_form_data_payload_and_headers_with_boundary(
+ boundary: &str,
+ name: &str,
+ filename: Option,
+ content_type: Option,
+ file: Bytes,
+) -> (Bytes, HeaderMap) {
+ let mut buf = BytesMut::with_capacity(file.len() + 128);
+
+ let boundary_str = [BOUNDARY_PREFIX, boundary].concat();
+ let boundary = boundary_str.as_bytes();
+
+ buf.put(HYPHENS);
+ buf.put(boundary);
+ buf.put(CRLF);
+
+ buf.put(format!("Content-Disposition: form-data; name=\"{name}\"").as_bytes());
+ if let Some(filename) = filename {
+ buf.put(format!("; filename=\"{filename}\"").as_bytes());
+ }
+ buf.put(CRLF);
+
+ if let Some(ct) = content_type {
+ buf.put(format!("Content-Type: {ct}").as_bytes());
+ buf.put(CRLF);
+ }
+
+ buf.put(format!("Content-Length: {}", file.len()).as_bytes());
+ buf.put(CRLF_CRLF);
+
+ buf.put(file);
+ buf.put(CRLF);
+
+ buf.put(HYPHENS);
+ buf.put(boundary);
+ buf.put(HYPHENS);
+ buf.put(CRLF);
+
+ let mut headers = HeaderMap::new();
+ headers.insert(
+ header::CONTENT_TYPE,
+ format!("multipart/form-data; boundary=\"{boundary_str}\"")
+ .parse()
+ .unwrap(),
+ );
+
+ (buf.freeze(), headers)
+}
+
+#[cfg(test)]
+mod tests {
+ use std::convert::Infallible;
+
+ use futures_util::stream;
+
+ use super::*;
+
+ fn find_boundary(headers: &HeaderMap) -> String {
+ headers
+ .get("content-type")
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .parse::()
+ .unwrap()
+ .get_param(mime::BOUNDARY)
+ .unwrap()
+ .as_str()
+ .to_owned()
+ }
+
+ #[test]
+ fn wire_format() {
+ let (pl, headers) = create_form_data_payload_and_headers_with_boundary(
+ "qWeRtYuIoP",
+ "foo",
+ None,
+ None,
+ Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
+ );
+
+ assert_eq!(
+ find_boundary(&headers),
+ "------------------------qWeRtYuIoP",
+ );
+
+ assert_eq!(
+ std::str::from_utf8(&pl).unwrap(),
+ "--------------------------qWeRtYuIoP\r\n\
+ Content-Disposition: form-data; name=\"foo\"\r\n\
+ Content-Length: 26\r\n\
+ \r\n\
+ Lorem ipsum dolor\n\
+ sit ame.\r\n\
+ --------------------------qWeRtYuIoP--\r\n",
+ );
+
+ let (pl, _headers) = create_form_data_payload_and_headers_with_boundary(
+ "qWeRtYuIoP",
+ "foo",
+ Some("Lorem.txt".to_owned()),
+ Some(mime::TEXT_PLAIN_UTF_8),
+ Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
+ );
+
+ assert_eq!(
+ std::str::from_utf8(&pl).unwrap(),
+ "--------------------------qWeRtYuIoP\r\n\
+ Content-Disposition: form-data; name=\"foo\"; filename=\"Lorem.txt\"\r\n\
+ Content-Type: text/plain; charset=utf-8\r\n\
+ Content-Length: 26\r\n\
+ \r\n\
+ Lorem ipsum dolor\n\
+ sit ame.\r\n\
+ --------------------------qWeRtYuIoP--\r\n",
+ );
+ }
+
+ /// Test using an external library to prevent the two-wrongs-make-a-right class of errors.
+ #[actix_web::test]
+ async fn ecosystem_compat() {
+ let (pl, headers) = create_form_data_payload_and_headers(
+ "foo",
+ None,
+ None,
+ Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
+ );
+
+ let boundary = find_boundary(&headers);
+
+ let pl = stream::once(async { Ok::<_, Infallible>(pl) });
+
+ let mut form = multer::Multipart::new(pl, boundary);
+ let field = form.next_field().await.unwrap().unwrap();
+ assert_eq!(field.name().unwrap(), "foo");
+ assert_eq!(field.file_name(), None);
+ assert_eq!(field.content_type(), None);
+ assert!(field.bytes().await.unwrap().starts_with(b"Lorem"));
+ }
+}
diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md
index 31316ff47..6305b45c3 100644
--- a/actix-router/CHANGES.md
+++ b/actix-router/CHANGES.md
@@ -2,6 +2,13 @@
## Unreleased
+## 0.5.3
+
+- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size.
+- Minimum supported Rust version (MSRV) is now 1.72.
+
+## 0.5.2
+
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
## 0.5.1
diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml
index 7fc800a94..7e7e3beb8 100644
--- a/actix-router/Cargo.toml
+++ b/actix-router/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-router"
-version = "0.5.1"
+version = "0.5.3"
authors = [
"Nikolay Kim ",
"Ali MJ Al-Nasrawy ",
@@ -8,7 +8,7 @@ authors = [
]
description = "Resource path matching and router"
keywords = ["actix", "router", "routing"]
-repository = "https://github.com/actix/actix-web.git"
+repository = "https://github.com/actix/actix-web"
license = "MIT OR Apache-2.0"
edition = "2021"
@@ -19,12 +19,16 @@ allowed_external_types = [
]
[features]
-default = ["http"]
+default = ["http", "unicode"]
+http = ["dep:http"]
+unicode = ["dep:regex"]
[dependencies]
bytestring = ">=0.1.5, <2"
+cfg-if = "1"
http = { version = "0.2.7", optional = true }
-regex = "1.5"
+regex = { version = "1.5", optional = true }
+regex-lite = "0.1"
serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
@@ -37,6 +41,7 @@ percent-encoding = "2.1"
[[bench]]
name = "router"
harness = false
+required-features = ["unicode"]
[[bench]]
name = "quoter"
diff --git a/actix-router/README.md b/actix-router/README.md
new file mode 100644
index 000000000..12d1b0146
--- /dev/null
+++ b/actix-router/README.md
@@ -0,0 +1,20 @@
+# `actix-router`
+
+
+
+[](https://crates.io/crates/actix-router)
+[](https://docs.rs/actix-router/0.5.3)
+
+
+
+[](https://deps.rs/crate/actix-router/0.5.3)
+[](https://crates.io/crates/actix-router)
+[](https://discord.gg/NWpN5mmg3x)
+
+
+
+
+
+Resource path matching and router.
+
+
diff --git a/actix-router/benches/quoter.rs b/actix-router/benches/quoter.rs
index c78240809..2428a767d 100644
--- a/actix-router/benches/quoter.rs
+++ b/actix-router/benches/quoter.rs
@@ -1,5 +1,3 @@
-#![allow(clippy::uninlined_format_args)]
-
use std::{borrow::Cow, fmt::Write as _};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs
index e8c7c658e..ce2dcf8f3 100644
--- a/actix-router/src/de.rs
+++ b/actix-router/src/de.rs
@@ -500,10 +500,10 @@ impl<'de> de::VariantAccess<'de> for UnitVariant {
#[cfg(test)]
mod tests {
- use serde::{de, Deserialize};
+ use serde::Deserialize;
use super::*;
- use crate::{path::Path, router::Router, ResourceDef};
+ use crate::{router::Router, ResourceDef};
#[derive(Deserialize)]
struct MyStruct {
diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs
index 53c0ad82a..c4d0d2c87 100644
--- a/actix-router/src/lib.rs
+++ b/actix-router/src/lib.rs
@@ -2,7 +2,6 @@
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
-#![allow(clippy::uninlined_format_args)]
#![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))]
@@ -11,6 +10,7 @@ mod de;
mod path;
mod pattern;
mod quoter;
+mod regex_set;
mod resource;
mod resource_path;
mod router;
diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs
index dc4150ddc..9031ab763 100644
--- a/actix-router/src/path.rs
+++ b/actix-router/src/path.rs
@@ -3,7 +3,7 @@ use std::{
ops::{DerefMut, Index},
};
-use serde::de;
+use serde::{de, Deserialize};
use crate::{de::PathDeserializer, Resource, ResourcePath};
@@ -24,8 +24,13 @@ impl Default for PathItem {
/// If resource path contains variable patterns, `Path` stores them.
#[derive(Debug, Clone, Default)]
pub struct Path {
+ /// Full path representation.
path: T,
+
+ /// Number of characters in `path` that have been processed into `segments`.
pub(crate) skip: u16,
+
+ /// List of processed dynamic segments; name->value pairs.
pub(crate) segments: Vec<(Cow<'static, str>, PathItem)>,
}
@@ -83,8 +88,8 @@ impl Path {
/// Set new path.
#[inline]
pub fn set(&mut self, path: T) {
- self.skip = 0;
self.path = path;
+ self.skip = 0;
self.segments.clear();
}
@@ -103,7 +108,7 @@ impl Path {
pub(crate) fn add(&mut self, name: impl Into>, value: PathItem) {
match value {
- PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))),
+ PathItem::Static(seg) => self.segments.push((name.into(), PathItem::Static(seg))),
PathItem::Segment(begin, end) => self.segments.push((
name.into(),
PathItem::Segment(self.skip + begin, self.skip + end),
@@ -149,15 +154,11 @@ impl Path {
None
}
- /// Get matched parameter by name.
+ /// Returns matched parameter by name.
///
/// If keyed parameter is not available empty string is used as default value.
pub fn query(&self, key: &str) -> &str {
- if let Some(s) = self.get(key) {
- s
- } else {
- ""
- }
+ self.get(key).unwrap_or_default()
}
/// Return iterator to items in parameter container.
@@ -168,9 +169,13 @@ impl Path {
}
}
- /// Try to deserialize matching parameters to a specified type `U`
- pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result {
- de::Deserialize::deserialize(PathDeserializer::new(self))
+ /// Deserializes matching parameters to a specified type `U`.
+ ///
+ /// # Errors
+ ///
+ /// Returns error when dynamic path segments cannot be deserialized into a `U` type.
+ pub fn load<'de, U: Deserialize<'de>>(&'de self) -> Result {
+ Deserialize::deserialize(PathDeserializer::new(self))
}
}
diff --git a/actix-router/src/regex_set.rs b/actix-router/src/regex_set.rs
new file mode 100644
index 000000000..48f38df2c
--- /dev/null
+++ b/actix-router/src/regex_set.rs
@@ -0,0 +1,66 @@
+//! Abstraction over `regex` and `regex-lite` depending on whether we have `unicode` crate feature
+//! enabled.
+
+use cfg_if::cfg_if;
+#[cfg(feature = "unicode")]
+pub(crate) use regex::{escape, Regex};
+#[cfg(not(feature = "unicode"))]
+pub(crate) use regex_lite::{escape, Regex};
+
+#[cfg(feature = "unicode")]
+#[derive(Debug, Clone)]
+pub(crate) struct RegexSet(regex::RegexSet);
+
+#[cfg(not(feature = "unicode"))]
+#[derive(Debug, Clone)]
+pub(crate) struct RegexSet(Vec);
+
+impl RegexSet {
+ /// Create a new regex set.
+ ///
+ /// # Panics
+ ///
+ /// Panics if any path patterns are malformed.
+ pub(crate) fn new(re_set: Vec) -> Self {
+ cfg_if! {
+ if #[cfg(feature = "unicode")] {
+ Self(regex::RegexSet::new(re_set).unwrap())
+ } else {
+ Self(re_set.iter().map(|re| Regex::new(re).unwrap()).collect())
+ }
+ }
+ }
+
+ /// Create a new empty regex set.
+ pub(crate) fn empty() -> Self {
+ cfg_if! {
+ if #[cfg(feature = "unicode")] {
+ Self(regex::RegexSet::empty())
+ } else {
+ Self(Vec::new())
+ }
+ }
+ }
+
+ /// Returns true if regex set matches `path`.
+ pub(crate) fn is_match(&self, path: &str) -> bool {
+ cfg_if! {
+ if #[cfg(feature = "unicode")] {
+ self.0.is_match(path)
+ } else {
+ self.0.iter().any(|re| re.is_match(path))
+ }
+ }
+ }
+
+ /// Returns index within `path` of first match.
+ pub(crate) fn first_match_idx(&self, path: &str) -> Option {
+ cfg_if! {
+ if #[cfg(feature = "unicode")] {
+ self.0.matches(path).into_iter().next()
+ } else {
+ Some(self.0.iter().enumerate().find(|(_, re)| re.is_match(path))?.0)
+ }
+ }
+ }
+}
diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs
index 80c0a2d68..3a102945b 100644
--- a/actix-router/src/resource.rs
+++ b/actix-router/src/resource.rs
@@ -5,10 +5,13 @@ use std::{
mem,
};
-use regex::{escape, Regex, RegexSet};
use tracing::error;
-use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
+use crate::{
+ path::PathItem,
+ regex_set::{escape, Regex, RegexSet},
+ IntoPatterns, Patterns, Resource, ResourcePath,
+};
const MAX_DYNAMIC_SEGMENTS: usize = 16;
@@ -193,8 +196,8 @@ const REGEX_FLAGS: &str = "(?s-m)";
/// # Trailing Slashes
/// It should be noted that this library takes no steps to normalize intra-path or trailing slashes.
/// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if
-/// they you wish to accommodate "recoverable" path errors. Below are several examples of
-/// resource-path pairs that would not be compatible.
+/// you wish to accommodate "recoverable" path errors. Below are several examples of resource-path
+/// pairs that would not be compatible.
///
/// ## Examples
/// ```
@@ -233,7 +236,7 @@ enum PatternSegment {
Var(String),
}
-#[derive(Clone, Debug)]
+#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum PatternType {
/// Single constant/literal segment.
@@ -603,7 +606,7 @@ impl ResourceDef {
PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()),
PatternType::DynamicSet(re, params) => {
- let idx = re.matches(path).into_iter().next()?;
+ let idx = re.first_match_idx(path)?;
let (ref pattern, _) = params[idx];
Some(pattern.captures(path)?[1].len())
}
@@ -706,7 +709,7 @@ impl ResourceDef {
PatternType::DynamicSet(re, params) => {
let path = path.unprocessed();
- let (pattern, names) = match re.matches(path).into_iter().next() {
+ let (pattern, names) = match re.first_match_idx(path) {
Some(idx) => ¶ms[idx],
_ => return false,
};
@@ -870,7 +873,7 @@ impl ResourceDef {
}
}
- let pattern_re_set = RegexSet::new(re_set).unwrap();
+ let pattern_re_set = RegexSet::new(re_set);
let segments = segments.unwrap_or_default();
(
diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs
index d31d10ce8..1dd4449da 100644
--- a/actix-router/src/router.rs
+++ b/actix-router/src/router.rs
@@ -97,6 +97,7 @@ impl RouterBuilder {
ctx: U,
) -> (&mut ResourceDef, &mut T, &mut U) {
self.routes.push((rdef, val, ctx));
+ #[allow(clippy::map_identity)] // map is used to distribute &mut-ness to tuple elements
self.routes
.last_mut()
.map(|(rdef, val, ctx)| (rdef, val, ctx))
@@ -186,11 +187,11 @@ mod tests {
assert_eq!(path.get("file").unwrap(), "file");
assert_eq!(path.get("ext").unwrap(), "gz");
- let mut path = Path::new("/vtest/ttt/index.html");
+ let mut path = Path::new("/v2/ttt/index.html");
let (h, info) = router.recognize_mut(&mut path).unwrap();
assert_eq!(*h, 14);
assert_eq!(info, ResourceId(4));
- assert_eq!(path.get("val").unwrap(), "test");
+ assert_eq!(path.get("val").unwrap(), "2");
assert_eq!(path.get("val2").unwrap(), "ttt");
let mut path = Path::new("/v/blah-blah/index.html");
diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md
index a3ca7fe10..ec2dd6776 100644
--- a/actix-test/CHANGES.md
+++ b/actix-test/CHANGES.md
@@ -2,6 +2,21 @@
## Unreleased
+## 0.1.5
+
+- Add `TestServerConfig::listen_address()` method.
+
+## 0.1.4
+
+- Add `TestServerConfig::rustls_0_23()` method for Rustls v0.23 support behind new `rustls-0_23` crate feature.
+- Add `TestServerConfig::disable_redirects()` method.
+- Various types from `awc`, such as `ClientRequest` and `ClientResponse`, are now re-exported.
+- Minimum supported Rust version (MSRV) is now 1.72.
+
+## 0.1.3
+
+- Add `TestServerConfig::rustls_0_22()` method for Rustls v0.22 support behind new `rustls-0_22` crate feature.
+
## 0.1.2
- Add `TestServerConfig::rustls_021()` method for Rustls v0.21 support behind new `rustls-0_21` crate feature.
diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml
index 5cb12810a..e810ae80b 100644
--- a/actix-test/Cargo.toml
+++ b/actix-test/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-test"
-version = "0.1.2"
+version = "0.1.5"
authors = [
"Nikolay Kim ",
"Rob Ede ",
@@ -8,7 +8,7 @@ authors = [
description = "Integration testing tools for Actix Web applications"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web.git"
+repository = "https://github.com/actix/actix-web"
categories = [
"network-programming",
"asynchronous",
@@ -43,19 +43,23 @@ rustls = ["rustls-0_20"]
rustls-0_20 = ["tls-rustls-0_20", "actix-http/rustls-0_20", "awc/rustls-0_20"]
# TLS via Rustls v0.21
rustls-0_21 = ["tls-rustls-0_21", "actix-http/rustls-0_21", "awc/rustls-0_21"]
+# TLS via Rustls v0.22
+rustls-0_22 = ["tls-rustls-0_22", "actix-http/rustls-0_22", "awc/rustls-0_22-webpki-roots"]
+# TLS via Rustls v0.23
+rustls-0_23 = ["tls-rustls-0_23", "actix-http/rustls-0_23", "awc/rustls-0_23-webpki-roots"]
# TLS via OpenSSL
openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies]
actix-codec = "0.5"
-actix-http = "3"
+actix-http = "3.7"
actix-http-test = "3"
actix-rt = "2.1"
actix-service = "2"
actix-utils = "3"
-actix-web = { version = "4", default-features = false, features = ["cookies"] }
-awc = { version = "3", default-features = false, features = ["cookies"] }
+actix-web = { version = "4.6", default-features = false, features = ["cookies"] }
+awc = { version = "3.5", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.17", default-features = false, features = ["std"] }
futures-util = { version = "0.3.17", default-features = false, features = [] }
@@ -66,4 +70,6 @@ serde_urlencoded = "0.7"
tls-openssl = { package = "openssl", version = "0.10.55", optional = true }
tls-rustls-0_20 = { package = "rustls", version = "0.20", optional = true }
tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true }
+tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true }
+tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true }
tokio = { version = "1.24.2", features = ["sync"] }
diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs
index e570bb266..803320607 100644
--- a/actix-test/src/lib.rs
+++ b/actix-test/src/lib.rs
@@ -52,7 +52,7 @@ use actix_web::{
rt::{self, System},
web, Error,
};
-use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
+pub use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
use futures_core::Stream;
use tokio::sync::mpsc;
@@ -143,12 +143,18 @@ where
StreamType::Rustls020(_) => true,
#[cfg(feature = "rustls-0_21")]
StreamType::Rustls021(_) => true,
+ #[cfg(feature = "rustls-0_22")]
+ StreamType::Rustls022(_) => true,
+ #[cfg(feature = "rustls-0_23")]
+ StreamType::Rustls023(_) => true,
};
+ let client_cfg = cfg.clone();
+
// run server in separate orphaned thread
thread::spawn(move || {
rt::System::new().block_on(async move {
- let tcp = net::TcpListener::bind(("127.0.0.1", cfg.port)).unwrap();
+ let tcp = net::TcpListener::bind((cfg.listen_address.clone(), cfg.port)).unwrap();
let local_addr = tcp.local_addr().unwrap();
let factory = factory.clone();
let srv_cfg = cfg.clone();
@@ -327,6 +333,90 @@ where
.rustls_021(config.clone())
}),
},
+ #[cfg(feature = "rustls-0_22")]
+ StreamType::Rustls022(config) => match cfg.tp {
+ HttpVer::Http1 => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .h1(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_22(config.clone())
+ }),
+ HttpVer::Http2 => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .h2(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_22(config.clone())
+ }),
+ HttpVer::Both => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .finish(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_22(config.clone())
+ }),
+ },
+ #[cfg(feature = "rustls-0_23")]
+ StreamType::Rustls023(config) => match cfg.tp {
+ HttpVer::Http1 => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .h1(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_23(config.clone())
+ }),
+ HttpVer::Http2 => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .h2(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_23(config.clone())
+ }),
+ HttpVer::Both => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .finish(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_23(config.clone())
+ }),
+ },
}
.expect("test server could not be created");
@@ -372,7 +462,13 @@ where
}
};
- Client::builder().connector(connector).finish()
+ let mut client_builder = Client::builder().connector(connector);
+
+ if client_cfg.disable_redirects {
+ client_builder = client_builder.disable_redirects();
+ }
+
+ client_builder.finish()
};
TestServer {
@@ -392,6 +488,7 @@ enum HttpVer {
Both,
}
+#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
enum StreamType {
Tcp,
@@ -401,6 +498,10 @@ enum StreamType {
Rustls020(tls_rustls_0_20::ServerConfig),
#[cfg(feature = "rustls-0_21")]
Rustls021(tls_rustls_0_21::ServerConfig),
+ #[cfg(feature = "rustls-0_22")]
+ Rustls022(tls_rustls_0_22::ServerConfig),
+ #[cfg(feature = "rustls-0_23")]
+ Rustls023(tls_rustls_0_23::ServerConfig),
}
/// Create default test server config.
@@ -413,8 +514,10 @@ pub struct TestServerConfig {
tp: HttpVer,
stream: StreamType,
client_request_timeout: Duration,
+ listen_address: String,
port: u16,
workers: usize,
+ disable_redirects: bool,
}
impl Default for TestServerConfig {
@@ -424,56 +527,96 @@ impl Default for TestServerConfig {
}
impl TestServerConfig {
- /// Create default server configuration
+ /// Constructs default server configuration.
pub(crate) fn new() -> TestServerConfig {
TestServerConfig {
tp: HttpVer::Both,
stream: StreamType::Tcp,
client_request_timeout: Duration::from_secs(5),
+ listen_address: "127.0.0.1".to_string(),
port: 0,
workers: 1,
+ disable_redirects: false,
}
}
- /// Accept HTTP/1.1 only.
+ /// Accepts HTTP/1.1 only.
pub fn h1(mut self) -> Self {
self.tp = HttpVer::Http1;
self
}
- /// Accept HTTP/2 only.
+ /// Accepts HTTP/2 only.
pub fn h2(mut self) -> Self {
self.tp = HttpVer::Http2;
self
}
- /// Accept secure connections via OpenSSL.
+ /// Accepts secure connections via OpenSSL.
#[cfg(feature = "openssl")]
pub fn openssl(mut self, acceptor: openssl::ssl::SslAcceptor) -> Self {
self.stream = StreamType::Openssl(acceptor);
self
}
- /// Accept secure connections via Rustls.
+ #[doc(hidden)]
+ #[deprecated(note = "Renamed to `rustls_0_20()`.")]
#[cfg(feature = "rustls-0_20")]
pub fn rustls(mut self, config: tls_rustls_0_20::ServerConfig) -> Self {
self.stream = StreamType::Rustls020(config);
self
}
- /// Accept secure connections via Rustls.
+ /// Accepts secure connections via Rustls v0.20.
+ #[cfg(feature = "rustls-0_20")]
+ pub fn rustls_0_20(mut self, config: tls_rustls_0_20::ServerConfig) -> Self {
+ self.stream = StreamType::Rustls020(config);
+ self
+ }
+
+ #[doc(hidden)]
+ #[deprecated(note = "Renamed to `rustls_0_21()`.")]
#[cfg(feature = "rustls-0_21")]
pub fn rustls_021(mut self, config: tls_rustls_0_21::ServerConfig) -> Self {
self.stream = StreamType::Rustls021(config);
self
}
- /// Set client timeout for first request.
+ /// Accepts secure connections via Rustls v0.21.
+ #[cfg(feature = "rustls-0_21")]
+ pub fn rustls_0_21(mut self, config: tls_rustls_0_21::ServerConfig) -> Self {
+ self.stream = StreamType::Rustls021(config);
+ self
+ }
+
+ /// Accepts secure connections via Rustls v0.22.
+ #[cfg(feature = "rustls-0_22")]
+ pub fn rustls_0_22(mut self, config: tls_rustls_0_22::ServerConfig) -> Self {
+ self.stream = StreamType::Rustls022(config);
+ self
+ }
+
+ /// Accepts secure connections via Rustls v0.23.
+ #[cfg(feature = "rustls-0_23")]
+ pub fn rustls_0_23(mut self, config: tls_rustls_0_23::ServerConfig) -> Self {
+ self.stream = StreamType::Rustls023(config);
+ self
+ }
+
+ /// Sets client timeout for first request.
pub fn client_request_timeout(mut self, dur: Duration) -> Self {
self.client_request_timeout = dur;
self
}
+ /// Sets the address the server will listen on.
+ ///
+ /// By default, only listens on `127.0.0.1`.
+ pub fn listen_address(mut self, addr: impl Into) -> Self {
+ self.listen_address = addr.into();
+ self
+ }
+
/// Sets test server port.
///
/// By default, a random free port is determined by the OS.
@@ -489,6 +632,15 @@ impl TestServerConfig {
self.workers = workers;
self
}
+
+ /// Instruct the client to not follow redirects.
+ ///
+ /// By default, the client will follow up to 10 consecutive redirects
+ /// before giving up.
+ pub fn disable_redirects(mut self) -> Self {
+ self.disable_redirects = true;
+ self
+ }
}
/// A basic HTTP server controller that simplifies the process of writing integration tests for
@@ -515,9 +667,9 @@ impl TestServer {
let scheme = if self.tls { "https" } else { "http" };
if uri.starts_with('/') {
- format!("{}://localhost:{}{}", scheme, self.addr.port(), uri)
+ format!("{}://{}{}", scheme, self.addr, uri)
} else {
- format!("{}://localhost:{}/{}", scheme, self.addr.port(), uri)
+ format!("{}://{}/{}", scheme, self.addr, uri)
}
}
diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md
index 5c516db56..3e854c0b8 100644
--- a/actix-web-actors/CHANGES.md
+++ b/actix-web-actors/CHANGES.md
@@ -2,6 +2,11 @@
## Unreleased
+- Take the encoded buffer when yielding bytes in the response stream rather than splitting the buffer, reducing memory use
+- Minimum supported Rust version (MSRV) is now 1.72.
+
+## 4.3.0
+
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
## 4.2.0
diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml
index 696d539b8..3c74a4f47 100644
--- a/actix-web-actors/Cargo.toml
+++ b/actix-web-actors/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-web-actors"
-version = "4.2.0"
+version = "4.3.0"
authors = ["Nikolay Kim "]
description = "Actix actors support for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"]
@@ -38,6 +38,6 @@ actix-test = "0.1"
awc = { version = "3", default-features = false }
actix-web = { version = "4", features = ["macros"] }
-env_logger = "0.10"
+env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
mime = "0.3"
diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md
index b2c30b954..feb3d1b33 100644
--- a/actix-web-actors/README.md
+++ b/actix-web-actors/README.md
@@ -1,17 +1,16 @@
-# actix-web-actors
+# `actix-web-actors`
> Actix actors support for Actix Web.
+
+
[](https://crates.io/crates/actix-web-actors)
-[](https://docs.rs/actix-web-actors/4.2.0)
-
+[](https://docs.rs/actix-web-actors/4.3.0)
+

-[](https://deps.rs/crate/actix-web-actors/4.2.0)
+[](https://deps.rs/crate/actix-web-actors/4.3.0)
[](https://crates.io/crates/actix-web-actors)
[](https://discord.gg/NWpN5mmg3x)
-## Documentation & Resources
-
-- [API Documentation](https://docs.rs/actix-web-actors)
-- Minimum Supported Rust Version (MSRV): 1.68
+
diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs
index be8fd387c..23e336459 100644
--- a/actix-web-actors/src/context.rs
+++ b/actix-web-actors/src/context.rs
@@ -248,13 +248,11 @@ where
mod tests {
use std::time::Duration;
- use actix::Actor;
use actix_web::{
http::StatusCode,
test::{call_service, init_service, read_body, TestRequest},
web, App, HttpResponse,
};
- use bytes::Bytes;
use super::*;
diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs
index cf2eb3645..d89b0ee35 100644
--- a/actix-web-actors/src/lib.rs
+++ b/actix-web-actors/src/lib.rs
@@ -57,7 +57,6 @@
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
-#![allow(clippy::uninlined_format_args)]
#![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))]
diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs
index 04dbf5e17..7f7607fa9 100644
--- a/actix-web-actors/src/ws.rs
+++ b/actix-web-actors/src/ws.rs
@@ -710,7 +710,7 @@ where
}
if !this.buf.is_empty() {
- Poll::Ready(Some(Ok(this.buf.split().freeze())))
+ Poll::Ready(Some(Ok(std::mem::take(&mut this.buf).freeze())))
} else if this.fut.alive() && !this.closed {
Poll::Pending
} else {
@@ -817,10 +817,7 @@ where
#[cfg(test)]
mod tests {
- use actix_web::{
- http::{header, Method},
- test::TestRequest,
- };
+ use actix_web::test::TestRequest;
use super::*;
diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md
index 00e36b037..d143723f4 100644
--- a/actix-web-codegen/CHANGES.md
+++ b/actix-web-codegen/CHANGES.md
@@ -2,6 +2,13 @@
## Unreleased
+## 4.3.0
+
+- Add `#[scope]` macro.
+- Add `compat-routing-macros-force-pub` crate feature which, on-by-default, which when disabled causes handlers to inherit their attached function's visibility.
+- Prevent inclusion of default `actix-router` features.
+- Minimum supported Rust version (MSRV) is now 1.72.
+
## 4.2.2
- Fix regression when declaring `wrap` attribute using an expression.
diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml
index 748984b49..7500807d2 100644
--- a/actix-web-codegen/Cargo.toml
+++ b/actix-web-codegen/Cargo.toml
@@ -1,21 +1,26 @@
[package]
name = "actix-web-codegen"
-version = "4.2.2"
+version = "4.3.0"
description = "Routing and runtime macros for Actix Web"
-homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web.git"
authors = [
"Nikolay Kim ",
"Rob Ede ",
]
-license = "MIT OR Apache-2.0"
-edition = "2021"
+homepage.workspace = true
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
[lib]
proc-macro = true
+[features]
+default = ["compat-routing-macros-force-pub"]
+compat-routing-macros-force-pub = []
+
[dependencies]
-actix-router = "0.5"
+actix-router = { version = "0.5", default-features = false }
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full", "extra-traits"] }
diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md
index e9a1f9c7e..e61bf5c74 100644
--- a/actix-web-codegen/README.md
+++ b/actix-web-codegen/README.md
@@ -1,20 +1,19 @@
-# actix-web-codegen
+# `actix-web-codegen`
> Routing and runtime macros for Actix Web.
+
+
[](https://crates.io/crates/actix-web-codegen)
-[](https://docs.rs/actix-web-codegen/4.2.2)
-
+[](https://docs.rs/actix-web-codegen/4.3.0)
+

-[](https://deps.rs/crate/actix-web-codegen/4.2.2)
+[](https://deps.rs/crate/actix-web-codegen/4.3.0)
[](https://crates.io/crates/actix-web-codegen)
[](https://discord.gg/NWpN5mmg3x)
-## Documentation & Resources
-
-- [API Documentation](https://docs.rs/actix-web-codegen)
-- Minimum Supported Rust Version (MSRV): 1.68
+
## Compile Testing
diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs
index 6d6c9ab5c..c518007a0 100644
--- a/actix-web-codegen/src/lib.rs
+++ b/actix-web-codegen/src/lib.rs
@@ -83,6 +83,7 @@ use proc_macro::TokenStream;
use quote::quote;
mod route;
+mod scope;
/// Creates resource handler, allowing multiple HTTP method guards.
///
@@ -197,6 +198,43 @@ method_macro!(Options, options);
method_macro!(Trace, trace);
method_macro!(Patch, patch);
+/// Prepends a path prefix to all handlers using routing macros inside the attached module.
+///
+/// # Syntax
+///
+/// ```
+/// # use actix_web_codegen::scope;
+/// #[scope("/prefix")]
+/// mod api {
+/// // ...
+/// }
+/// ```
+///
+/// # Arguments
+///
+/// - `"/prefix"` - Raw literal string to be prefixed onto contained handlers' paths.
+///
+/// # Example
+///
+/// ```
+/// # use actix_web_codegen::{scope, get};
+/// # use actix_web::Responder;
+/// #[scope("/api")]
+/// mod api {
+/// # use super::*;
+/// #[get("/hello")]
+/// pub async fn hello() -> impl Responder {
+/// // this has path /api/hello
+/// "Hello, world!"
+/// }
+/// }
+/// # fn main() {}
+/// ```
+#[proc_macro_attribute]
+pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream {
+ scope::with_scope(args, input)
+}
+
/// Marks async main function as the Actix Web system entry-point.
///
/// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is
@@ -240,3 +278,15 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
output.extend(item);
output
}
+
+/// Converts the error to a token stream and appends it to the original input.
+///
+/// Returning the original input in addition to the error is good for IDEs which can gracefully
+/// recover and show more precise errors within the macro body.
+///
+/// See for more info.
+fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
+ let compile_err = TokenStream::from(err.to_compile_error());
+ item.extend(compile_err);
+ item
+}
diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs
index 7a2dfc051..e24903e3a 100644
--- a/actix-web-codegen/src/route.rs
+++ b/actix-web-codegen/src/route.rs
@@ -6,10 +6,12 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token};
+use crate::input_and_compile_error;
+
#[derive(Debug)]
pub struct RouteArgs {
- path: syn::LitStr,
- options: Punctuated,
+ pub(crate) path: syn::LitStr,
+ pub(crate) options: Punctuated,
}
impl syn::parse::Parse for RouteArgs {
@@ -78,7 +80,7 @@ macro_rules! standard_method_type {
}
}
- fn from_path(method: &Path) -> Result {
+ pub(crate) fn from_path(method: &Path) -> Result {
match () {
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
_ => Err(()),
@@ -411,6 +413,13 @@ impl ToTokens for Route {
doc_attributes,
} = self;
+ #[allow(unused_variables)] // used when force-pub feature is disabled
+ let vis = &ast.vis;
+
+ // TODO(breaking): remove this force-pub forwards-compatibility feature
+ #[cfg(feature = "compat-routing-macros-force-pub")]
+ let vis = syn::Visibility::Public(::default());
+
let registrations: TokenStream2 = args
.iter()
.map(|args| {
@@ -458,7 +467,7 @@ impl ToTokens for Route {
let stream = quote! {
#(#doc_attributes)*
#[allow(non_camel_case_types, missing_docs)]
- pub struct #name;
+ #vis struct #name;
impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) {
@@ -542,15 +551,3 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
Err(err) => input_and_compile_error(input, err),
}
}
-
-/// Converts the error to a token stream and appends it to the original input.
-///
-/// Returning the original input in addition to the error is good for IDEs which can gracefully
-/// recover and show more precise errors within the macro body.
-///
-/// See for more info.
-fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
- let compile_err = TokenStream::from(err.to_compile_error());
- item.extend(compile_err);
- item
-}
diff --git a/actix-web-codegen/src/scope.rs b/actix-web-codegen/src/scope.rs
new file mode 100644
index 000000000..067d95a60
--- /dev/null
+++ b/actix-web-codegen/src/scope.rs
@@ -0,0 +1,103 @@
+use proc_macro::TokenStream;
+use proc_macro2::{Span, TokenStream as TokenStream2};
+use quote::{quote, ToTokens as _};
+
+use crate::{
+ input_and_compile_error,
+ route::{MethodType, RouteArgs},
+};
+
+pub fn with_scope(args: TokenStream, input: TokenStream) -> TokenStream {
+ match with_scope_inner(args, input.clone()) {
+ Ok(stream) => stream,
+ Err(err) => input_and_compile_error(input, err),
+ }
+}
+
+fn with_scope_inner(args: TokenStream, input: TokenStream) -> syn::Result {
+ if args.is_empty() {
+ return Err(syn::Error::new(
+ Span::call_site(),
+ "missing arguments for scope macro, expected: #[scope(\"/prefix\")]",
+ ));
+ }
+
+ let scope_prefix = syn::parse::(args.clone()).map_err(|err| {
+ syn::Error::new(
+ err.span(),
+ "argument to scope macro is not a string literal, expected: #[scope(\"/prefix\")]",
+ )
+ })?;
+
+ let scope_prefix_value = scope_prefix.value();
+
+ if scope_prefix_value.ends_with('/') {
+ // trailing slashes cause non-obvious problems
+ // it's better to point them out to developers rather than
+
+ return Err(syn::Error::new(
+ scope_prefix.span(),
+ "scopes should not have trailing slashes; see https://docs.rs/actix-web/4/actix_web/struct.Scope.html#avoid-trailing-slashes",
+ ));
+ }
+
+ let mut module = syn::parse::(input).map_err(|err| {
+ syn::Error::new(err.span(), "#[scope] macro must be attached to a module")
+ })?;
+
+ // modify any routing macros (method or route[s]) attached to
+ // functions by prefixing them with this scope macro's argument
+ if let Some((_, items)) = &mut module.content {
+ for item in items {
+ if let syn::Item::Fn(fun) = item {
+ fun.attrs = fun
+ .attrs
+ .iter()
+ .map(|attr| modify_attribute_with_scope(attr, &scope_prefix_value))
+ .collect();
+ }
+ }
+ }
+
+ Ok(module.to_token_stream().into())
+}
+
+/// Checks if the attribute is a method type and has a route path, then modifies it.
+fn modify_attribute_with_scope(attr: &syn::Attribute, scope_path: &str) -> syn::Attribute {
+ match (attr.parse_args::(), attr.clone().meta) {
+ (Ok(route_args), syn::Meta::List(meta_list)) if has_allowed_methods_in_scope(attr) => {
+ let modified_path = format!("{}{}", scope_path, route_args.path.value());
+
+ let options_tokens: Vec = route_args
+ .options
+ .iter()
+ .map(|option| {
+ quote! { ,#option }
+ })
+ .collect();
+
+ let combined_options_tokens: TokenStream2 =
+ options_tokens
+ .into_iter()
+ .fold(TokenStream2::new(), |mut acc, ts| {
+ acc.extend(std::iter::once(ts));
+ acc
+ });
+
+ syn::Attribute {
+ meta: syn::Meta::List(syn::MetaList {
+ tokens: quote! { #modified_path #combined_options_tokens },
+ ..meta_list.clone()
+ }),
+ ..attr.clone()
+ }
+ }
+ _ => attr.clone(),
+ }
+}
+
+fn has_allowed_methods_in_scope(attr: &syn::Attribute) -> bool {
+ MethodType::from_path(attr.path()).is_ok()
+ || attr.path().is_ident("route")
+ || attr.path().is_ident("ROUTE")
+}
diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/routes.rs
similarity index 100%
rename from actix-web-codegen/tests/test_macro.rs
rename to actix-web-codegen/tests/routes.rs
diff --git a/actix-web-codegen/tests/scopes.rs b/actix-web-codegen/tests/scopes.rs
new file mode 100644
index 000000000..4ee6db16f
--- /dev/null
+++ b/actix-web-codegen/tests/scopes.rs
@@ -0,0 +1,200 @@
+use actix_web::{guard::GuardContext, http, http::header, web, App, HttpResponse, Responder};
+use actix_web_codegen::{delete, get, post, route, routes, scope};
+
+pub fn image_guard(ctx: &GuardContext) -> bool {
+ ctx.header::()
+ .map(|h| h.preference() == "image/*")
+ .unwrap_or(false)
+}
+
+#[scope("/test")]
+mod scope_module {
+ // ensure that imports can be brought into the scope
+ use super::*;
+
+ #[get("/test/guard", guard = "image_guard")]
+ pub async fn guard() -> impl Responder {
+ HttpResponse::Ok()
+ }
+
+ #[get("/test")]
+ pub async fn test() -> impl Responder {
+ HttpResponse::Ok().finish()
+ }
+
+ #[get("/twice-test/{value}")]
+ pub async fn twice(value: web::Path) -> impl actix_web::Responder {
+ let int_value: i32 = value.parse().unwrap_or(0);
+ let doubled = int_value * 2;
+ HttpResponse::Ok().body(format!("Twice value: {}", doubled))
+ }
+
+ #[post("/test")]
+ pub async fn post() -> impl Responder {
+ HttpResponse::Ok().body("post works")
+ }
+
+ #[delete("/test")]
+ pub async fn delete() -> impl Responder {
+ "delete works"
+ }
+
+ #[route("/test", method = "PUT", method = "PATCH", method = "CUSTOM")]
+ pub async fn multiple_shared_path() -> impl Responder {
+ HttpResponse::Ok().finish()
+ }
+
+ #[routes]
+ #[head("/test1")]
+ #[connect("/test2")]
+ #[options("/test3")]
+ #[trace("/test4")]
+ pub async fn multiple_separate_paths() -> impl Responder {
+ HttpResponse::Ok().finish()
+ }
+
+ // test calling this from other mod scope with scope attribute...
+ pub fn mod_common(message: String) -> impl actix_web::Responder {
+ HttpResponse::Ok().body(message)
+ }
+}
+
+/// Scope doc string to check in cargo expand.
+#[scope("/v1")]
+mod mod_scope_v1 {
+ use super::*;
+
+ /// Route doc string to check in cargo expand.
+ #[get("/test")]
+ pub async fn test() -> impl Responder {
+ scope_module::mod_common("version1 works".to_string())
+ }
+}
+
+#[scope("/v2")]
+mod mod_scope_v2 {
+ use super::*;
+
+ // check to make sure non-function tokens in the scope block are preserved...
+ enum TestEnum {
+ Works,
+ }
+
+ #[get("/test")]
+ pub async fn test() -> impl Responder {
+ // make sure this type still exists...
+ let test_enum = TestEnum::Works;
+
+ match test_enum {
+ TestEnum::Works => scope_module::mod_common("version2 works".to_string()),
+ }
+ }
+}
+
+#[actix_rt::test]
+async fn scope_get_async() {
+ let srv = actix_test::start(|| App::new().service(scope_module::test));
+
+ let request = srv.request(http::Method::GET, srv.url("/test/test"));
+ let response = request.send().await.unwrap();
+ assert!(response.status().is_success());
+}
+
+#[actix_rt::test]
+async fn scope_get_param_async() {
+ let srv = actix_test::start(|| App::new().service(scope_module::twice));
+
+ let request = srv.request(http::Method::GET, srv.url("/test/twice-test/4"));
+ let mut response = request.send().await.unwrap();
+ let body = response.body().await.unwrap();
+ let body_str = String::from_utf8(body.to_vec()).unwrap();
+ assert_eq!(body_str, "Twice value: 8");
+}
+
+#[actix_rt::test]
+async fn scope_post_async() {
+ let srv = actix_test::start(|| App::new().service(scope_module::post));
+
+ let request = srv.request(http::Method::POST, srv.url("/test/test"));
+ let mut response = request.send().await.unwrap();
+ let body = response.body().await.unwrap();
+ let body_str = String::from_utf8(body.to_vec()).unwrap();
+ assert_eq!(body_str, "post works");
+}
+
+#[actix_rt::test]
+async fn multiple_shared_path_async() {
+ let srv = actix_test::start(|| App::new().service(scope_module::multiple_shared_path));
+
+ let request = srv.request(http::Method::PUT, srv.url("/test/test"));
+ let response = request.send().await.unwrap();
+ assert!(response.status().is_success());
+
+ let request = srv.request(http::Method::PATCH, srv.url("/test/test"));
+ let response = request.send().await.unwrap();
+ assert!(response.status().is_success());
+}
+
+#[actix_rt::test]
+async fn multiple_multi_path_async() {
+ let srv = actix_test::start(|| App::new().service(scope_module::multiple_separate_paths));
+
+ let request = srv.request(http::Method::HEAD, srv.url("/test/test1"));
+ let response = request.send().await.unwrap();
+ assert!(response.status().is_success());
+
+ let request = srv.request(http::Method::CONNECT, srv.url("/test/test2"));
+ let response = request.send().await.unwrap();
+ assert!(response.status().is_success());
+
+ let request = srv.request(http::Method::OPTIONS, srv.url("/test/test3"));
+ let response = request.send().await.unwrap();
+ assert!(response.status().is_success());
+
+ let request = srv.request(http::Method::TRACE, srv.url("/test/test4"));
+ let response = request.send().await.unwrap();
+ assert!(response.status().is_success());
+}
+
+#[actix_rt::test]
+async fn scope_delete_async() {
+ let srv = actix_test::start(|| App::new().service(scope_module::delete));
+
+ let request = srv.request(http::Method::DELETE, srv.url("/test/test"));
+ let mut response = request.send().await.unwrap();
+ let body = response.body().await.unwrap();
+ let body_str = String::from_utf8(body.to_vec()).unwrap();
+ assert_eq!(body_str, "delete works");
+}
+
+#[actix_rt::test]
+async fn scope_get_with_guard_async() {
+ let srv = actix_test::start(|| App::new().service(scope_module::guard));
+
+ let request = srv
+ .request(http::Method::GET, srv.url("/test/test/guard"))
+ .insert_header(("Accept", "image/*"));
+ let response = request.send().await.unwrap();
+ assert!(response.status().is_success());
+}
+
+#[actix_rt::test]
+async fn scope_v1_v2_async() {
+ let srv = actix_test::start(|| {
+ App::new()
+ .service(mod_scope_v1::test)
+ .service(mod_scope_v2::test)
+ });
+
+ let request = srv.request(http::Method::GET, srv.url("/v1/test"));
+ let mut response = request.send().await.unwrap();
+ let body = response.body().await.unwrap();
+ let body_str = String::from_utf8(body.to_vec()).unwrap();
+ assert_eq!(body_str, "version1 works");
+
+ let request = srv.request(http::Method::GET, srv.url("/v2/test"));
+ let mut response = request.send().await.unwrap();
+ let body = response.body().await.unwrap();
+ let body_str = String::from_utf8(body.to_vec()).unwrap();
+ assert_eq!(body_str, "version2 works");
+}
diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs
index 8e1f58a4c..91073cf3b 100644
--- a/actix-web-codegen/tests/trybuild.rs
+++ b/actix-web-codegen/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();
@@ -18,6 +18,11 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
+ t.compile_fail("tests/trybuild/scope-on-handler.rs");
+ t.compile_fail("tests/trybuild/scope-missing-args.rs");
+ t.compile_fail("tests/trybuild/scope-invalid-args.rs");
+ t.compile_fail("tests/trybuild/scope-trailing-slash.rs");
+
t.pass("tests/trybuild/docstring-ok.rs");
t.pass("tests/trybuild/test-runtime.rs");
diff --git a/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr
index 88198a55d..c2a51d005 100644
--- a/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr
+++ b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr
@@ -13,17 +13,20 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future