",
@@ -71,9 +71,9 @@ actix-service = "2"
actix-utils = "3"
actix-tls = { version = "3", default-features = false, optional = true }
-actix-http = { version = "3", features = ["http2", "ws"] }
+actix-http = { version = "3.2.2", features = ["http2", "ws"] }
actix-router = "0.5"
-actix-web-codegen = { version = "4", optional = true }
+actix-web-codegen = { version = "4.1", optional = true }
ahash = "0.7"
bytes = "1"
diff --git a/actix-web/README.md b/actix-web/README.md
index fdd4a8648..65076e0b8 100644
--- a/actix-web/README.md
+++ b/actix-web/README.md
@@ -6,10 +6,10 @@
[](https://crates.io/crates/actix-web)
-[](https://docs.rs/actix-web/4.1.0)
-
+[](https://docs.rs/actix-web/4.2.1)
+

-[](https://deps.rs/crate/actix-web/4.1.0)
+[](https://deps.rs/crate/actix-web/4.2.1)
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[](https://codecov.io/gh/actix/actix-web)
@@ -33,7 +33,7 @@
- SSL support using OpenSSL or Rustls
- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
- Integrates with the [`awc` HTTP client](https://docs.rs/awc/)
-- Runs on stable Rust 1.57+
+- Runs on stable Rust 1.59+
## Documentation
diff --git a/actix-web/src/http/header/content_disposition.rs b/actix-web/src/http/header/content_disposition.rs
index 0bb459193..f743302a2 100644
--- a/actix-web/src/http/header/content_disposition.rs
+++ b/actix-web/src/http/header/content_disposition.rs
@@ -79,7 +79,7 @@ impl<'a> From<&'a str> for DispositionType {
/// assert!(param.is_filename());
/// assert_eq!(param.as_filename().unwrap(), "sample.txt");
/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub enum DispositionParam {
/// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from
@@ -302,7 +302,7 @@ impl DispositionParam {
/// change to match local file system conventions if applicable, and do not use directory path
/// information that may be present.
/// See [RFC 2183 ยง2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3).
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentDisposition {
/// The disposition type
pub disposition: DispositionType,
@@ -312,16 +312,36 @@ pub struct ContentDisposition {
}
impl ContentDisposition {
+ /// Constructs a Content-Disposition header suitable for downloads.
+ ///
+ /// # Examples
+ /// ```
+ /// use actix_web::http::header::{ContentDisposition, TryIntoHeaderValue as _};
+ ///
+ /// let cd = ContentDisposition::attachment("files.zip");
+ ///
+ /// let cd_val = cd.try_into_value().unwrap();
+ /// assert_eq!(cd_val, "attachment; filename=\"files.zip\"");
+ /// ```
+ pub fn attachment(filename: impl Into) -> Self {
+ Self {
+ disposition: DispositionType::Attachment,
+ parameters: vec![DispositionParam::Filename(filename.into())],
+ }
+ }
+
/// Parse a raw Content-Disposition header value.
pub fn from_raw(hv: &header::HeaderValue) -> Result {
// `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible
// ASCII characters. So `hv.as_bytes` is necessary here.
let hv = String::from_utf8(hv.as_bytes().to_vec())
.map_err(|_| crate::error::ParseError::Header)?;
+
let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';');
if disp_type.is_empty() {
return Err(crate::error::ParseError::Header);
}
+
let mut cd = ContentDisposition {
disposition: disp_type.into(),
parameters: Vec::new(),
diff --git a/actix-web/src/scope.rs b/actix-web/src/scope.rs
index f8c042a5f..07eb1093a 100644
--- a/actix-web/src/scope.rs
+++ b/actix-web/src/scope.rs
@@ -40,7 +40,7 @@ type Guards = Vec>;
/// use actix_web::{web, App, HttpResponse};
///
/// let app = App::new().service(
-/// web::scope("/{project_id}/")
+/// web::scope("/{project_id}")
/// .service(web::resource("/path1").to(|| async { "OK" }))
/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok())))
/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed)))
diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs
index 5021e0a2d..3a8897f11 100644
--- a/actix-web/src/server.rs
+++ b/actix-web/src/server.rs
@@ -172,7 +172,7 @@ where
///
/// One thread pool is set up **per worker**; not shared across workers.
///
- /// By default set to 512 / workers.
+ /// By default set to 512 divided by the number of workers.
pub fn worker_max_blocking_threads(mut self, num: usize) -> Self {
self.builder = self.builder.worker_max_blocking_threads(num);
self
diff --git a/awc/CHANGES.md b/awc/CHANGES.md
index e229a6d96..7892d9339 100644
--- a/awc/CHANGES.md
+++ b/awc/CHANGES.md
@@ -2,8 +2,18 @@
## Unreleased - 2022-xx-xx
### Changed
+- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
+
+
+## 3.0.1 - 2022-08-25
+### Changed
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
+### Fixed
+- Fixed handling of redirection requests that begin with `//`. [#2840]
+
+[#2840]: https://github.com/actix/actix-web/pull/2840
+
## 3.0.0 - 2022-03-07
### Dependencies
diff --git a/awc/Cargo.toml b/awc/Cargo.toml
index 0250091bf..2f0027725 100644
--- a/awc/Cargo.toml
+++ b/awc/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "awc"
-version = "3.0.0"
+version = "3.0.1"
authors = [
"Nikolay Kim ",
"fakeshadow <24548779@qq.com>",
diff --git a/awc/README.md b/awc/README.md
index db70f7332..9f47e663b 100644
--- a/awc/README.md
+++ b/awc/README.md
@@ -3,9 +3,9 @@
> Async HTTP and WebSocket client library.
[](https://crates.io/crates/awc)
-[](https://docs.rs/awc/3.0.0)
+[](https://docs.rs/awc/3.0.1)

-[](https://deps.rs/crate/awc/3.0.0)
+[](https://deps.rs/crate/awc/3.0.1)
[](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources
diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs
index d48822168..67ef5d76f 100644
--- a/awc/src/middleware/redirect.rs
+++ b/awc/src/middleware/redirect.rs
@@ -257,6 +257,16 @@ fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result = scheme.as_bytes().to_vec();
+ full_url.push(b':');
+ full_url.extend(location.as_bytes());
+
+ return Uri::try_from(full_url)
+ .map_err(|_| SendRequestError::Url(InvalidUrl::MissingScheme));
+ }
// when scheme or authority is missing treat the location value as path and query
// recover error where location does not have leading slash
let path = if location.as_bytes().starts_with(b"/") {
@@ -588,6 +598,41 @@ mod tests {
assert_eq!(res.status().as_u16(), 200);
}
+ #[actix_rt::test]
+ async fn test_double_slash_redirect() {
+ let client = ClientBuilder::new()
+ .disable_redirects()
+ .wrap(Redirect::new().max_redirect_times(10))
+ .finish();
+
+ let srv = actix_test::start(|| {
+ App::new()
+ .service(web::resource("/test").route(web::to(|| async {
+ Ok::<_, Error>(HttpResponse::BadRequest())
+ })))
+ .service(
+ web::resource("/").route(web::to(|req: HttpRequest| async move {
+ Ok::<_, Error>(
+ HttpResponse::Found()
+ .append_header((
+ "location",
+ format!(
+ "//localhost:{}/test",
+ req.app_config().local_addr().port()
+ )
+ .as_str(),
+ ))
+ .finish(),
+ )
+ })),
+ )
+ });
+
+ let res = client.get(srv.url("/")).send().await.unwrap();
+
+ assert_eq!(res.status().as_u16(), 400);
+ }
+
#[actix_rt::test]
async fn test_remove_sensitive_headers() {
fn gen_headers() -> header::HeaderMap {
diff --git a/clippy.toml b/clippy.toml
index 5cccb362c..abe19b3a0 100644
--- a/clippy.toml
+++ b/clippy.toml
@@ -1 +1 @@
-msrv = "1.57"
+msrv = "1.59"
diff --git a/scripts/unreleased b/scripts/unreleased
index e664c0879..6738ad090 100755
--- a/scripts/unreleased
+++ b/scripts/unreleased
@@ -45,6 +45,8 @@ unreleased_for() {
cat "$CHANGE_CHUNK_FILE"
}
-for f in $(fd --absolute-path 'CHANGE\w+.md'); do
- unreleased_for $(dirname $f)
+files=$(fd --threads=1 --min-depth=2 --absolute-path 'CHANGE\w+.md')
+
+for f in $files; do
+ unreleased_for $(dirname $f) || true
done