Actix web
- Actix web is a powerful, pragmatic, and extremely fast web framework for Rust
+ Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust
[](https://crates.io/crates/actix-web)
-[](https://docs.rs/actix-web/3.2.0)
+[](https://docs.rs/actix-web/3.3.2)
[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)

-[](https://deps.rs/crate/actix-web/3.2.0)
+[](https://deps.rs/crate/actix-web/3.3.2)
[](https://travis-ci.org/actix/actix-web)
[](https://codecov.io/gh/actix/actix-web)
diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md
index 978d1c69d..c4d56010f 100644
--- a/actix-files/CHANGES.md
+++ b/actix-files/CHANGES.md
@@ -3,6 +3,10 @@
## Unreleased - 2020-xx-xx
+## 0.4.1 - 2020-11-24
+* Clarify order of parameters in `Files::new` and improve docs.
+
+
## 0.4.0 - 2020-10-06
* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml
index c829887ba..f7d32f8ec 100644
--- a/actix-files/Cargo.toml
+++ b/actix-files/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-files"
-version = "0.4.0"
+version = "0.4.1"
authors = ["Nikolay Kim "]
description = "Static file serving for Actix Web"
readme = "README.md"
@@ -21,14 +21,14 @@ actix-web = { version = "3.0.0", default-features = false }
actix-service = "1.0.6"
bitflags = "1"
bytes = "0.5.3"
-futures-core = { version = "0.3.5", default-features = false }
-futures-util = { version = "0.3.5", default-features = false }
+futures-core = { version = "0.3.7", default-features = false }
+futures-util = { version = "0.3.7", default-features = false }
derive_more = "0.99.2"
log = "0.4"
mime = "0.3"
mime_guess = "2.0.1"
percent-encoding = "2.1"
-v_htmlescape = "0.10"
+v_htmlescape = "0.11"
[dev-dependencies]
actix-rt = "1.0.0"
diff --git a/actix-files/README.md b/actix-files/README.md
index d31439361..685e5dbe5 100644
--- a/actix-files/README.md
+++ b/actix-files/README.md
@@ -2,12 +2,12 @@
> Static file serving for Actix Web
-[](https://crates.io/crates/actix-files)
-[](https://docs.rs/actix-files)
+[](https://crates.io/crates/actix-files)
+[](https://docs.rs/actix-files/0.4.1)
[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)

-[](https://deps.rs/crate/actix-files/0.4.0)
+[](https://deps.rs/crate/actix-files/0.4.1)
[](https://crates.io/crates/actix-files)
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs
index 5a783e2dd..a99b4699e 100644
--- a/actix-files/src/files.rs
+++ b/actix-files/src/files.rs
@@ -65,13 +65,25 @@ impl Clone for Files {
}
impl Files {
- /// Create new `Files` instance for specified base directory.
+ /// Create new `Files` instance for a specified base directory.
///
- /// `File` uses `ThreadPool` for blocking filesystem operations.
- /// By default pool with 5x threads of available cpus is used.
- /// Pool size can be changed by setting ACTIX_THREADPOOL environment variable.
- pub fn new>(path: &str, dir: T) -> Files {
- let orig_dir = dir.into();
+ /// # Argument Order
+ /// The first argument (`mount_path`) is the root URL at which the static files are served.
+ /// For example, `/assets` will serve files at `example.com/assets/...`.
+ ///
+ /// The second argument (`serve_from`) is the location on disk at which files are loaded.
+ /// This can be a relative path. For example, `./` would serve files from the current
+ /// working directory.
+ ///
+ /// # Implementation Notes
+ /// If the mount path is set as the root path `/`, services registered after this one will
+ /// be inaccessible. Register more specific handlers and services first.
+ ///
+ /// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a
+ /// number of threads equal to 5x the number of available logical CPUs. Pool size can be changed
+ /// by setting ACTIX_THREADPOOL environment variable.
+ pub fn new>(mount_path: &str, serve_from: T) -> Files {
+ let orig_dir = serve_from.into();
let dir = match orig_dir.canonicalize() {
Ok(canon_dir) => canon_dir,
Err(_) => {
@@ -81,7 +93,7 @@ impl Files {
};
Files {
- path: path.to_string(),
+ path: mount_path.to_owned(),
directory: dir,
index: None,
show_index: false,
diff --git a/test-server/CHANGES.md b/actix-http-test/CHANGES.md
similarity index 69%
rename from test-server/CHANGES.md
rename to actix-http-test/CHANGES.md
index 845b6e2dc..835b75ddc 100644
--- a/test-server/CHANGES.md
+++ b/actix-http-test/CHANGES.md
@@ -2,15 +2,20 @@
## Unreleased - 2020-xx-xx
-* add ability to set address for `TestServer` [#1645]
-* Upgrade `base64` to `0.13`.
-* Upgrade `serde_urlencoded` to `0.7`.
+## 2.1.0 - 2020-11-25
+* Add ability to set address for `TestServer`. [#1645]
+* Upgrade `base64` to `0.13`.
+* Upgrade `serde_urlencoded` to `0.7`. [#1773]
+
+[#1773]: https://github.com/actix/actix-web/pull/1773
[#1645]: https://github.com/actix/actix-web/pull/1645
+
## 2.0.0 - 2020-09-11
* Update actix-codec and actix-utils dependencies.
+
## 2.0.0-alpha.1 - 2020-05-23
* Update the `time` dependency to 0.2.7
* Update `actix-connect` dependency to 2.0.0-alpha.2
@@ -20,74 +25,56 @@
* Update `base64` dependency to 0.12
* Update `env_logger` dependency to 0.7
-## [1.0.0] - 2019-12-13
-
-### Changed
-
+## 1.0.0 - 2019-12-13
* Replaced `TestServer::start()` with `test_server()`
-## [1.0.0-alpha.3] - 2019-12-07
-
-### Changed
-
+## 1.0.0-alpha.3 - 2019-12-07
* Migrate to `std::future`
-## [0.2.5] - 2019-09-17
-
-### Changed
-
+## 0.2.5 - 2019-09-17
* Update serde_urlencoded to "0.6.1"
* Increase TestServerRuntime timeouts from 500ms to 3000ms
-
-### Fixed
-
* Do not override current `System`
-## [0.2.4] - 2019-07-18
-
+## 0.2.4 - 2019-07-18
* Update actix-server to 0.6
-## [0.2.3] - 2019-07-16
+## 0.2.3 - 2019-07-16
* Add `delete`, `options`, `patch` methods to `TestServerRunner`
-## [0.2.2] - 2019-06-16
+## 0.2.2 - 2019-06-16
* Add .put() and .sput() methods
-## [0.2.1] - 2019-06-05
+## 0.2.1 - 2019-06-05
* Add license files
-## [0.2.0] - 2019-05-12
+## 0.2.0 - 2019-05-12
* Update awc and actix-http deps
-## [0.1.1] - 2019-04-24
+## 0.1.1 - 2019-04-24
* Always make new connection for http client
-## [0.1.0] - 2019-04-16
-
+## 0.1.0 - 2019-04-16
* No changes
-## [0.1.0-alpha.3] - 2019-04-02
-
+## 0.1.0-alpha.3 - 2019-04-02
* Request functions accept path #743
-## [0.1.0-alpha.2] - 2019-03-29
-
+## 0.1.0-alpha.2 - 2019-03-29
* Added TestServerRuntime::load_body() method
-
* Update actix-http and awc libraries
-## [0.1.0-alpha.1] - 2019-03-28
-
+## 0.1.0-alpha.1 - 2019-03-28
* Initial impl
diff --git a/test-server/Cargo.toml b/actix-http-test/Cargo.toml
similarity index 93%
rename from test-server/Cargo.toml
rename to actix-http-test/Cargo.toml
index 87db93469..8b23bef1c 100644
--- a/test-server/Cargo.toml
+++ b/actix-http-test/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "actix-http-test"
-version = "2.0.0"
+version = "2.1.0"
authors = ["Nikolay Kim "]
-description = "Actix HTTP test server"
+description = "Various helpers for Actix applications to use during testing"
readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
diff --git a/test-server/LICENSE-APACHE b/actix-http-test/LICENSE-APACHE
similarity index 100%
rename from test-server/LICENSE-APACHE
rename to actix-http-test/LICENSE-APACHE
diff --git a/test-server/LICENSE-MIT b/actix-http-test/LICENSE-MIT
similarity index 100%
rename from test-server/LICENSE-MIT
rename to actix-http-test/LICENSE-MIT
diff --git a/actix-http-test/README.md b/actix-http-test/README.md
new file mode 100644
index 000000000..c847c8515
--- /dev/null
+++ b/actix-http-test/README.md
@@ -0,0 +1,15 @@
+# actix-http-test
+
+> Various helpers for Actix applications to use during testing.
+
+[](https://crates.io/crates/actix-http-test)
+[](https://docs.rs/actix-http-test/2.1.0)
+
+[](https://deps.rs/crate/actix-http-test/2.1.0)
+[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+## Documentation & Resources
+
+- [API Documentation](https://docs.rs/actix-http-test)
+- [Chat on Gitter](https://gitter.im/actix/actix-web)
+- Minimum Supported Rust Version (MSRV): 1.42.0
diff --git a/test-server/src/lib.rs b/actix-http-test/src/lib.rs
similarity index 98%
rename from test-server/src/lib.rs
rename to actix-http-test/src/lib.rs
index 4159c8d86..f881dfb4c 100644
--- a/test-server/src/lib.rs
+++ b/actix-http-test/src/lib.rs
@@ -1,4 +1,9 @@
//! Various helpers for Actix applications to use during testing.
+
+#![deny(rust_2018_idioms)]
+#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
+#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
+
use std::sync::mpsc;
use std::{net, thread, time};
diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md
index eeed4e14b..c602ab2e1 100644
--- a/actix-http/CHANGES.md
+++ b/actix-http/CHANGES.md
@@ -1,17 +1,25 @@
# Changes
## Unreleased - 2020-xx-xx
+
+
+## 2.2.0 - 2020-11-25
### Added
* HttpResponse builders for 1xx status codes. [#1768]
+* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
+* `TryFrom` and `TryFrom` for `http::header::Quality`. [#1797]
### Fixed
* Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
### Changed
-* Upgrade `serde_urlencoded` to `0.7`.
+* Upgrade `serde_urlencoded` to `0.7`. [#1773]
+[#1773]: https://github.com/actix/actix-web/pull/1773
[#1767]: https://github.com/actix/actix-web/pull/1767
[#1768]: https://github.com/actix/actix-web/pull/1768
+[#1793]: https://github.com/actix/actix-web/pull/1793
+[#1797]: https://github.com/actix/actix-web/pull/1797
## 2.1.0 - 2020-10-30
diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml
index 31495e395..7375c6eb3 100644
--- a/actix-http/Cargo.toml
+++ b/actix-http/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-http"
-version = "2.1.0"
+version = "2.2.0"
authors = ["Nikolay Kim "]
description = "HTTP primitives for the Actix ecosystem"
readme = "README.md"
diff --git a/actix-http/README.md b/actix-http/README.md
index e536276ca..9103cd184 100644
--- a/actix-http/README.md
+++ b/actix-http/README.md
@@ -3,14 +3,14 @@
> HTTP primitives for the Actix ecosystem.
[](https://crates.io/crates/actix-http)
-[](https://docs.rs/actix-http/2.1.0)
+[](https://docs.rs/actix-http/2.2.0)

-[](https://deps.rs/crate/actix-http/2.1.0)
+[](https://deps.rs/crate/actix-http/2.2.0)
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
-- [API Documentation](https://docs.rs/actix-http/2.1.0)
+- [API Documentation](https://docs.rs/actix-http)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.42.0
diff --git a/actix-http/src/header/common/accept.rs b/actix-http/src/header/common/accept.rs
index d52eba241..da26b0261 100644
--- a/actix-http/src/header/common/accept.rs
+++ b/actix-http/src/header/common/accept.rs
@@ -1,3 +1,5 @@
+use std::cmp::Ordering;
+
use mime::Mime;
use crate::header::{qitem, QualityItem};
@@ -7,7 +9,7 @@ header! {
/// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2)
///
/// The `Accept` header field can be used by user agents to specify
- /// response media types that are acceptable. Accept header fields can
+ /// response media types that are acceptable. Accept header fields can
/// be used to indicate that the request is specifically limited to a
/// small set of desired types, as in the case of a request for an
/// in-line image
@@ -97,14 +99,14 @@ header! {
test_header!(
test1,
vec![b"audio/*; q=0.2, audio/basic"],
- Some(HeaderField(vec![
+ Some(Accept(vec![
QualityItem::new("audio/*".parse().unwrap(), q(200)),
qitem("audio/basic".parse().unwrap()),
])));
test_header!(
test2,
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
- Some(HeaderField(vec![
+ Some(Accept(vec![
QualityItem::new(mime::TEXT_PLAIN, q(500)),
qitem(mime::TEXT_HTML),
QualityItem::new(
@@ -138,23 +140,148 @@ header! {
}
impl Accept {
- /// A constructor to easily create `Accept: */*`.
+ /// Construct `Accept: */*`.
pub fn star() -> Accept {
Accept(vec![qitem(mime::STAR_STAR)])
}
- /// A constructor to easily create `Accept: application/json`.
+ /// Construct `Accept: application/json`.
pub fn json() -> Accept {
Accept(vec![qitem(mime::APPLICATION_JSON)])
}
- /// A constructor to easily create `Accept: text/*`.
+ /// Construct `Accept: text/*`.
pub fn text() -> Accept {
Accept(vec![qitem(mime::TEXT_STAR)])
}
- /// A constructor to easily create `Accept: image/*`.
+ /// Construct `Accept: image/*`.
pub fn image() -> Accept {
Accept(vec![qitem(mime::IMAGE_STAR)])
}
+
+ /// Construct `Accept: text/html`.
+ pub fn html() -> Accept {
+ Accept(vec![qitem(mime::TEXT_HTML)])
+ }
+
+ /// Returns a sorted list of mime types from highest to lowest preference, accounting for
+ /// [q-factor weighting] and specificity.
+ ///
+ /// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2
+ pub fn mime_precedence(&self) -> Vec {
+ let mut types = self.0.clone();
+
+ // use stable sort so items with equal q-factor and specificity retain listed order
+ types.sort_by(|a, b| {
+ // sort by q-factor descending
+ b.quality.cmp(&a.quality).then_with(|| {
+ // use specificity rules on mime types with
+ // same q-factor (eg. text/html > text/* > */*)
+
+ // subtypes are not comparable if main type is star, so return
+ match (a.item.type_(), b.item.type_()) {
+ (mime::STAR, mime::STAR) => return Ordering::Equal,
+
+ // a is sorted after b
+ (mime::STAR, _) => return Ordering::Greater,
+
+ // a is sorted before b
+ (_, mime::STAR) => return Ordering::Less,
+
+ _ => {}
+ }
+
+ // in both these match expressions, the returned ordering appears
+ // inverted because sort is high-to-low ("descending") precedence
+ match (a.item.subtype(), b.item.subtype()) {
+ (mime::STAR, mime::STAR) => Ordering::Equal,
+
+ // a is sorted after b
+ (mime::STAR, _) => Ordering::Greater,
+
+ // a is sorted before b
+ (_, mime::STAR) => Ordering::Less,
+
+ _ => Ordering::Equal,
+ }
+ })
+ });
+
+ types.into_iter().map(|qitem| qitem.item).collect()
+ }
+
+ /// Extracts the most preferable mime type, accounting for [q-factor weighting].
+ ///
+ /// If no q-factors are provided, the first mime type is chosen. Note that items without
+ /// q-factors are given the maximum preference value.
+ ///
+ /// Returns `None` if contained list is empty.
+ ///
+ /// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2
+ pub fn mime_preference(&self) -> Option {
+ let types = self.mime_precedence();
+ types.first().cloned()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::header::q;
+
+ #[test]
+ fn test_mime_precedence() {
+ let test = Accept(vec![]);
+ assert!(test.mime_precedence().is_empty());
+
+ let test = Accept(vec![qitem(mime::APPLICATION_JSON)]);
+ assert_eq!(test.mime_precedence(), vec!(mime::APPLICATION_JSON));
+
+ let test = Accept(vec![
+ qitem(mime::TEXT_HTML),
+ "application/xhtml+xml".parse().unwrap(),
+ QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
+ QualityItem::new(mime::STAR_STAR, q(0.8)),
+ ]);
+ assert_eq!(
+ test.mime_precedence(),
+ vec![
+ mime::TEXT_HTML,
+ "application/xhtml+xml".parse().unwrap(),
+ "application/xml".parse().unwrap(),
+ mime::STAR_STAR,
+ ]
+ );
+
+ let test = Accept(vec![
+ qitem(mime::STAR_STAR),
+ qitem(mime::IMAGE_STAR),
+ qitem(mime::IMAGE_PNG),
+ ]);
+ assert_eq!(
+ test.mime_precedence(),
+ vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR]
+ );
+ }
+
+ #[test]
+ fn test_mime_preference() {
+ let test = Accept(vec![
+ qitem(mime::TEXT_HTML),
+ "application/xhtml+xml".parse().unwrap(),
+ QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
+ QualityItem::new(mime::STAR_STAR, q(0.8)),
+ ]);
+ assert_eq!(test.mime_preference(), Some(mime::TEXT_HTML));
+
+ let test = Accept(vec![
+ QualityItem::new("video/*".parse().unwrap(), q(0.8)),
+ qitem(mime::IMAGE_PNG),
+ QualityItem::new(mime::STAR_STAR, q(0.5)),
+ qitem(mime::IMAGE_SVG),
+ QualityItem::new(mime::IMAGE_STAR, q(0.8)),
+ ]);
+ assert_eq!(test.mime_preference(), Some(mime::IMAGE_PNG));
+ }
}
diff --git a/actix-http/src/header/common/content_disposition.rs b/actix-http/src/header/common/content_disposition.rs
index 37da830ca..826cfef63 100644
--- a/actix-http/src/header/common/content_disposition.rs
+++ b/actix-http/src/header/common/content_disposition.rs
@@ -550,8 +550,7 @@ impl fmt::Display for ContentDisposition {
write!(f, "{}", self.disposition)?;
self.parameters
.iter()
- .map(|param| write!(f, "; {}", param))
- .collect()
+ .try_for_each(|param| write!(f, "; {}", param))
}
}
diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs
index 46fb31a62..0f87516eb 100644
--- a/actix-http/src/header/mod.rs
+++ b/actix-http/src/header/mod.rs
@@ -370,9 +370,7 @@ impl fmt::Display for ExtendedValue {
}
/// Percent encode a sequence of bytes with a character set defined in
-/// [https://tools.ietf.org/html/rfc5987#section-3.2][url]
-///
-/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2
+///
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
fmt::Display::fmt(&encoded, f)
diff --git a/actix-http/src/header/shared/charset.rs b/actix-http/src/header/shared/charset.rs
index 00e7309d4..36bdbf7e2 100644
--- a/actix-http/src/header/shared/charset.rs
+++ b/actix-http/src/header/shared/charset.rs
@@ -7,9 +7,7 @@ use self::Charset::*;
///
/// The string representation is normalized to upper case.
///
-/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url].
-///
-/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml
+/// See .
#[derive(Clone, Debug, PartialEq)]
#[allow(non_camel_case_types)]
pub enum Charset {
diff --git a/actix-http/src/header/shared/entity.rs b/actix-http/src/header/shared/entity.rs
index 3525a19c6..344cfb864 100644
--- a/actix-http/src/header/shared/entity.rs
+++ b/actix-http/src/header/shared/entity.rs
@@ -7,10 +7,12 @@ use crate::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer};
/// 1. `%x21`, or
/// 2. in the range `%x23` to `%x7E`, or
/// 3. above `%x80`
+fn entity_validate_char(c: u8) -> bool {
+ c == 0x21 || (0x23..=0x7e).contains(&c) || (c >= 0x80)
+}
+
fn check_slice_validity(slice: &str) -> bool {
- slice
- .bytes()
- .all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80'))
+ slice.bytes().all(entity_validate_char)
}
/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3)
diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs
index 98230dec1..01a3b988a 100644
--- a/actix-http/src/header/shared/quality_item.rs
+++ b/actix-http/src/header/shared/quality_item.rs
@@ -1,10 +1,17 @@
-use std::{cmp, fmt, str};
+use std::{
+ cmp,
+ convert::{TryFrom, TryInto},
+ fmt, str,
+};
-use self::internal::IntoQuality;
+use derive_more::{Display, Error};
+
+const MAX_QUALITY: u16 = 1000;
+const MAX_FLOAT_QUALITY: f32 = 1.0;
/// Represents a quality used in quality values.
///
-/// Can be created with the `q` function.
+/// Can be created with the [`q`] function.
///
/// # Implementation notes
///
@@ -18,12 +25,54 @@ use self::internal::IntoQuality;
///
/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1)
/// gives more information on quality values in HTTP header fields.
-#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Quality(u16);
+impl Quality {
+ /// # Panics
+ /// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
+ fn from_f32(value: f32) -> Self {
+ // Check that `value` is within range should be done before calling this method.
+ // Just in case, this debug_assert should catch if we were forgetful.
+ debug_assert!(
+ (0.0f32..=1.0f32).contains(&value),
+ "q value must be between 0.0 and 1.0"
+ );
+
+ Quality((value * MAX_QUALITY as f32) as u16)
+ }
+}
+
impl Default for Quality {
fn default() -> Quality {
- Quality(1000)
+ Quality(MAX_QUALITY)
+ }
+}
+
+#[derive(Debug, Clone, Display, Error)]
+pub struct QualityOutOfBounds;
+
+impl TryFrom for Quality {
+ type Error = QualityOutOfBounds;
+
+ fn try_from(value: u16) -> Result {
+ if (0..=MAX_QUALITY).contains(&value) {
+ Ok(Quality(value))
+ } else {
+ Err(QualityOutOfBounds)
+ }
+ }
+}
+
+impl TryFrom for Quality {
+ type Error = QualityOutOfBounds;
+
+ fn try_from(value: f32) -> Result {
+ if (0.0..=MAX_FLOAT_QUALITY).contains(&value) {
+ Ok(Quality::from_f32(value))
+ } else {
+ Err(QualityOutOfBounds)
+ }
}
}
@@ -55,8 +104,9 @@ impl cmp::PartialOrd for QualityItem {
impl fmt::Display for QualityItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.item, f)?;
+
match self.quality.0 {
- 1000 => Ok(()),
+ MAX_QUALITY => Ok(()),
0 => f.write_str("; q=0"),
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')),
}
@@ -66,105 +116,79 @@ impl fmt::Display for QualityItem {
impl str::FromStr for QualityItem {
type Err = crate::error::ParseError;
- fn from_str(s: &str) -> Result, crate::error::ParseError> {
- if !s.is_ascii() {
+ fn from_str(qitem_str: &str) -> Result, crate::error::ParseError> {
+ if !qitem_str.is_ascii() {
return Err(crate::error::ParseError::Header);
}
+
// Set defaults used if parsing fails.
- let mut raw_item = s;
+ let mut raw_item = qitem_str;
let mut quality = 1f32;
- let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect();
+ let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect();
+
if parts.len() == 2 {
+ // example for item with q-factor:
+ //
+ // gzip; q=0.65
+ // ^^^^^^ parts[0]
+ // ^^ start
+ // ^^^^ q_val
+ // ^^^^ parts[1]
+
if parts[0].len() < 2 {
+ // Can't possibly be an attribute since an attribute needs at least a name followed
+ // by an equals sign. And bare identifiers are forbidden.
return Err(crate::error::ParseError::Header);
}
+
let start = &parts[0][0..2];
+
if start == "q=" || start == "Q=" {
- let q_part = &parts[0][2..parts[0].len()];
- if q_part.len() > 5 {
+ let q_val = &parts[0][2..];
+ if q_val.len() > 5 {
+ // longer than 5 indicates an over-precise q-factor
return Err(crate::error::ParseError::Header);
}
- match q_part.parse::() {
- Ok(q_value) => {
- if 0f32 <= q_value && q_value <= 1f32 {
- quality = q_value;
- raw_item = parts[1];
- } else {
- return Err(crate::error::ParseError::Header);
- }
- }
- Err(_) => return Err(crate::error::ParseError::Header),
+
+ let q_value = q_val
+ .parse::()
+ .map_err(|_| crate::error::ParseError::Header)?;
+
+ if (0f32..=1f32).contains(&q_value) {
+ quality = q_value;
+ raw_item = parts[1];
+ } else {
+ return Err(crate::error::ParseError::Header);
}
}
}
- match raw_item.parse::() {
- // we already checked above that the quality is within range
- Ok(item) => Ok(QualityItem::new(item, from_f32(quality))),
- Err(_) => Err(crate::error::ParseError::Header),
- }
- }
-}
-#[inline]
-fn from_f32(f: f32) -> Quality {
- // this function is only used internally. A check that `f` is within range
- // should be done before calling this method. Just in case, this
- // debug_assert should catch if we were forgetful
- debug_assert!(
- f >= 0f32 && f <= 1f32,
- "q value must be between 0.0 and 1.0"
- );
- Quality((f * 1000f32) as u16)
+ let item = raw_item
+ .parse::()
+ .map_err(|_| crate::error::ParseError::Header)?;
+
+ // we already checked above that the quality is within range
+ Ok(QualityItem::new(item, Quality::from_f32(quality)))
+ }
}
/// Convenience function to wrap a value in a `QualityItem`
/// Sets `q` to the default 1.0
pub fn qitem(item: T) -> QualityItem {
- QualityItem::new(item, Default::default())
+ QualityItem::new(item, Quality::default())
}
/// Convenience function to create a `Quality` from a float or integer.
///
/// Implemented for `u16` and `f32`. Panics if value is out of range.
-pub fn q(val: T) -> Quality {
- val.into_quality()
-}
-
-mod internal {
- use super::Quality;
-
- // TryFrom is probably better, but it's not stable. For now, we want to
- // keep the functionality of the `q` function, while allowing it to be
- // generic over `f32` and `u16`.
- //
- // `q` would panic before, so keep that behavior. `TryFrom` can be
- // introduced later for a non-panicking conversion.
-
- pub trait IntoQuality: Sealed + Sized {
- fn into_quality(self) -> Quality;
- }
-
- impl IntoQuality for f32 {
- fn into_quality(self) -> Quality {
- assert!(
- self >= 0f32 && self <= 1f32,
- "float must be between 0.0 and 1.0"
- );
- super::from_f32(self)
- }
- }
-
- impl IntoQuality for u16 {
- fn into_quality(self) -> Quality {
- assert!(self <= 1000, "u16 must be between 0 and 1000");
- Quality(self)
- }
- }
-
- pub trait Sealed {}
- impl Sealed for u16 {}
- impl Sealed for f32 {}
+pub fn q(val: T) -> Quality
+where
+ T: TryInto,
+ T::Error: fmt::Debug,
+{
+ // TODO: on next breaking change, handle unwrap differently
+ val.try_into().unwrap()
}
#[cfg(test)]
@@ -270,15 +294,13 @@ mod tests {
}
#[test]
- #[should_panic] // FIXME - 32-bit msvc unwinding broken
- #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
+ #[should_panic]
fn test_quality_invalid() {
q(-1.0);
}
#[test]
- #[should_panic] // FIXME - 32-bit msvc unwinding broken
- #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
+ #[should_panic]
fn test_quality_invalid2() {
q(2.0);
}
diff --git a/awc/CHANGES.md b/awc/CHANGES.md
index e184dfbd1..7ca415336 100644
--- a/awc/CHANGES.md
+++ b/awc/CHANGES.md
@@ -1,8 +1,19 @@
# Changes
## Unreleased - 2020-xx-xx
+
+
+## 2.0.3 - 2020-11-29
+### Fixed
+* Ensure `actix-http` dependency uses same `serde_urlencoded`.
+
+
+## 2.0.2 - 2020-11-25
### Changed
-* Upgrade `serde_urlencoded` to `0.7`.
+* Upgrade `serde_urlencoded` to `0.7`. [#1773]
+
+[#1773]: https://github.com/actix/actix-web/pull/1773
+
## 2.0.1 - 2020-10-30
### Changed
diff --git a/awc/Cargo.toml b/awc/Cargo.toml
index d92996fb9..3c1963d6b 100644
--- a/awc/Cargo.toml
+++ b/awc/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "awc"
-version = "2.0.1"
+version = "2.0.3"
authors = ["Nikolay Kim "]
description = "Async HTTP and WebSocket client library built on the Actix ecosystem"
readme = "README.md"
@@ -39,7 +39,7 @@ compress = ["actix-http/compress"]
[dependencies]
actix-codec = "0.3.0"
actix-service = "1.0.6"
-actix-http = "2.0.0"
+actix-http = "2.2.0"
actix-rt = "1.0.0"
base64 = "0.13"
diff --git a/awc/README.md b/awc/README.md
index cbe299aaf..b97d4fa00 100644
--- a/awc/README.md
+++ b/awc/README.md
@@ -3,14 +3,14 @@
> Async HTTP and WebSocket client library.
[](https://crates.io/crates/awc)
-[](https://docs.rs/awc/2.0.1)
+[](https://docs.rs/awc/2.0.3)

-[](https://deps.rs/crate/awc/2.0.1)
+[](https://deps.rs/crate/awc/2.0.3)
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
-- [API Documentation](https://docs.rs/awc/2.0.1)
+- [API Documentation](https://docs.rs/awc)
- [Example Project](https://github.com/actix/examples/tree/HEAD/awc_https)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.42.0
diff --git a/awc/src/ws.rs b/awc/src/ws.rs
index 57e80bd46..aa474697b 100644
--- a/awc/src/ws.rs
+++ b/awc/src/ws.rs
@@ -70,9 +70,14 @@ impl WebsocketsRequest {
>::Error: Into,
{
let mut err = None;
- let mut head = RequestHead::default();
- head.method = Method::GET;
- head.version = Version::HTTP_11;
+
+ #[allow(clippy::field_reassign_with_default)]
+ let mut head = {
+ let mut head = RequestHead::default();
+ head.method = Method::GET;
+ head.version = Version::HTTP_11;
+ head
+ };
match Uri::try_from(uri) {
Ok(uri) => head.uri = uri,
diff --git a/codecov.yml b/codecov.yml
index 90cdfab47..102e8969d 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,4 +1,13 @@
-ignore: # ignore codecoverage on following paths
+coverage:
+ status:
+ project:
+ default:
+ threshold: 10% # make CI green
+ patch:
+ default:
+ threshold: 10% # make CI green
+
+ignore: # ignore code coverage on following paths
- "**/tests"
- "test-server"
- "**/benches"
diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot
index 7abd51268..17228fe62 100644
--- a/docs/graphs/web-focus.dot
+++ b/docs/graphs/web-focus.dot
@@ -2,29 +2,31 @@ digraph {
subgraph cluster_web {
label="actix/actix-web"
"awc"
- "actix-web"
- "actix-files"
- "actix-http"
- "actix-multipart"
- "actix-web-actors"
- "actix-web-codegen"
+ "web"
+ "files"
+ "http"
+ "multipart"
+ "web-actors"
+ "codegen"
+ "http-test"
}
- "actix-web" -> { "actix-codec" "actix-service" "actix-utils" "actix-router" "actix-rt" "actix-server" "actix-testing" "actix-macros" "actix-threadpool" "actix-tls" "actix-web-codegen" "actix-http" "awc" }
- "awc" -> { "actix-codec" "actix-service" "actix-http" "actix-rt" }
- "actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" }
- "actix-multipart" -> { "actix-web" "actix-service" "actix-utils" }
- "actix-http" -> { "actix-service" "actix-codec" "actix-connect" "actix-utils" "actix-rt" "actix-threadpool" }
- "actix-http" -> { "actix" "actix-tls" }[color=blue] // optional
- "actix-files" -> { "actix-web" "actix-http" }
+ "web" -> { "codec" "service" "utils" "router" "rt" "server" "testing" "macros" "threadpool" "tls" "codegen" "http" "awc" }
+ "awc" -> { "codec" "service" "http" "rt" }
+ "web-actors" -> { "actix" "web" "http" "codec" }
+ "multipart" -> { "web" "service" "utils" }
+ "http" -> { "service" "codec" "connect" "utils" "rt" "threadpool" }
+ "http" -> { "actix" "tls" }[color=blue] // optional
+ "files" -> { "web" }
+ "http-test" -> { "service" "codec" "connect" "utils" "rt" "server" "testing" "awc" }
// net
- "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" }
- "actix-tracing" -> { "actix-service" }
- "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" }
- "actix-testing" -> { "actix-rt" "actix-macros" "actix-server" "actix-service" }
- "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" }
- "actix-rt" -> { "actix-macros" "actix-threadpool" }
- "actix-connect" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" }
+ "utils" -> { "service" "rt" "codec" }
+ "tracing" -> { "service" }
+ "tls" -> { "service" "codec" "utils" }
+ "testing" -> { "rt" "macros" "server" "service" }
+ "server" -> { "service" "rt" "codec" "utils" }
+ "rt" -> { "macros" "threadpool" }
+ "connect" -> { "service" "codec" "utils" "rt" }
}
diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot
index 6e41fdc27..9e1bb2805 100644
--- a/docs/graphs/web-only.dot
+++ b/docs/graphs/web-only.dot
@@ -8,12 +8,14 @@ digraph {
"actix-multipart"
"actix-web-actors"
"actix-web-codegen"
+ "actix-http-test"
}
- "actix-web" -> { "actix-web-codegen" "actix-http" "awc" }
+ "actix-web" -> { "actix-web-codegen" "actix-http" "awc" }
"awc" -> { "actix-http" }
"actix-web-actors" -> { "actix" "actix-web" "actix-http" }
"actix-multipart" -> { "actix-web" }
"actix-http" -> { "actix" }[color=blue] // optional
- "actix-files" -> { "actix-web" "actix-http" }
+ "actix-files" -> { "actix-web" }
+ "actix-http-test" -> { "awc" }
}
diff --git a/src/error.rs b/src/error.rs
index 659ba05fd..60af8fa11 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,4 +1,5 @@
//! Error and Result module
+
pub use actix_http::error::*;
use derive_more::{Display, From};
use serde_json::error::Error as JsonError;
diff --git a/src/lib.rs b/src/lib.rs
index 088444e05..a8fc50d83 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,4 @@
-//! Actix web is a powerful, pragmatic, and extremely fast web framework for Rust.
+//! Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
//!
//! ## Example
//!
@@ -102,10 +102,11 @@ pub use crate::app::App;
pub use crate::extract::FromRequest;
pub use crate::request::HttpRequest;
pub use crate::resource::Resource;
-pub use crate::responder::{Either, Responder};
+pub use crate::responder::Responder;
pub use crate::route::Route;
pub use crate::scope::Scope;
pub use crate::server::HttpServer;
+pub use crate::types::{Either, EitherExtractError};
pub mod dev {
//! The `actix-web` prelude for library developers
diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs
index fe3ba841c..7575d7455 100644
--- a/src/middleware/compress.rs
+++ b/src/middleware/compress.rs
@@ -192,10 +192,7 @@ impl AcceptEncoding {
};
let quality = match parts.len() {
1 => encoding.quality(),
- _ => match f64::from_str(parts[1]) {
- Ok(q) => q,
- Err(_) => 0.0,
- },
+ _ => f64::from_str(parts[1]).unwrap_or(0.0),
};
Some(AcceptEncoding { encoding, quality })
}
diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs
index ab1c69746..9061c7458 100644
--- a/src/middleware/condition.rs
+++ b/src/middleware/condition.rs
@@ -105,6 +105,7 @@ mod tests {
use crate::test::{self, TestRequest};
use crate::HttpResponse;
+ #[allow(clippy::unnecessary_wraps)]
fn render_500(mut res: ServiceResponse) -> Result> {
res.response_mut()
.headers_mut()
diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs
index 93a5d3f22..c0cb9594e 100644
--- a/src/middleware/errhandlers.rs
+++ b/src/middleware/errhandlers.rs
@@ -154,6 +154,7 @@ mod tests {
use crate::test::{self, TestRequest};
use crate::HttpResponse;
+ #[allow(clippy::unnecessary_wraps)]
fn render_500(mut res: ServiceResponse) -> Result> {
res.response_mut()
.headers_mut()
diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs
index e0ecd90dc..ac8ad71d5 100644
--- a/src/middleware/normalize.rs
+++ b/src/middleware/normalize.rs
@@ -137,9 +137,9 @@ where
// so the change can not be deduced from the length comparison
if path != original_path {
let mut parts = head.uri.clone().into_parts();
- let pq = parts.path_and_query.as_ref().unwrap();
+ let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
- let path = if let Some(q) = pq.query() {
+ let path = if let Some(q) = query {
Bytes::from(format!("{}?{}", path, q))
} else {
Bytes::copy_from_slice(path.as_bytes())
diff --git a/src/request.rs b/src/request.rs
index a1b42f926..bd4bbbf58 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -675,4 +675,40 @@ mod tests {
let res = call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
}
+
+ #[actix_rt::test]
+ async fn extract_path_pattern_complex() {
+ let mut srv = init_service(
+ App::new()
+ .service(web::scope("/user").service(web::scope("/{id}").service(
+ web::resource("").to(move |req: HttpRequest| {
+ assert_eq!(req.match_pattern(), Some("/user/{id}".to_owned()));
+
+ HttpResponse::Ok().finish()
+ }),
+ )))
+ .service(web::resource("/").to(move |req: HttpRequest| {
+ assert_eq!(req.match_pattern(), Some("/".to_owned()));
+
+ HttpResponse::Ok().finish()
+ }))
+ .default_service(web::to(move |req: HttpRequest| {
+ assert!(req.match_pattern().is_none());
+ HttpResponse::Ok().finish()
+ })),
+ )
+ .await;
+
+ let req = TestRequest::get().uri("/user/test").to_request();
+ let res = call_service(&mut srv, req).await;
+ assert_eq!(res.status(), StatusCode::OK);
+
+ let req = TestRequest::get().uri("/").to_request();
+ let res = call_service(&mut srv, req).await;
+ assert_eq!(res.status(), StatusCode::OK);
+
+ let req = TestRequest::get().uri("/not-exist").to_request();
+ let res = call_service(&mut srv, req).await;
+ assert_eq!(res.status(), StatusCode::OK);
+ }
}
diff --git a/src/responder.rs b/src/responder.rs
index fc80831b8..d1c22323f 100644
--- a/src/responder.rs
+++ b/src/responder.rs
@@ -332,82 +332,6 @@ impl Future for CustomResponderFut {
}
}
-/// Combines two different responder types into a single type
-///
-/// ```rust
-/// use actix_web::{Either, Error, HttpResponse};
-///
-/// type RegisterResult = Either>;
-///
-/// fn index() -> RegisterResult {
-/// if is_a_variant() {
-/// // <- choose left variant
-/// Either::A(HttpResponse::BadRequest().body("Bad data"))
-/// } else {
-/// Either::B(
-/// // <- Right variant
-/// Ok(HttpResponse::Ok()
-/// .content_type("text/html")
-/// .body("Hello!"))
-/// )
-/// }
-/// }
-/// # fn is_a_variant() -> bool { true }
-/// # fn main() {}
-/// ```
-#[derive(Debug, PartialEq)]
-pub enum Either {
- /// First branch of the type
- A(A),
- /// Second branch of the type
- B(B),
-}
-
-impl Responder for Either
-where
- A: Responder,
- B: Responder,
-{
- type Error = Error;
- type Future = EitherResponder;
-
- fn respond_to(self, req: &HttpRequest) -> Self::Future {
- match self {
- Either::A(a) => EitherResponder::A(a.respond_to(req)),
- Either::B(b) => EitherResponder::B(b.respond_to(req)),
- }
- }
-}
-
-#[pin_project(project = EitherResponderProj)]
-pub enum EitherResponder
-where
- A: Responder,
- B: Responder,
-{
- A(#[pin] A::Future),
- B(#[pin] B::Future),
-}
-
-impl Future for EitherResponder
-where
- A: Responder,
- B: Responder,
-{
- type Output = Result;
-
- fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
- match self.project() {
- EitherResponderProj::A(fut) => {
- Poll::Ready(ready!(fut.poll(cx)).map_err(|e| e.into()))
- }
- EitherResponderProj::B(fut) => {
- Poll::Ready(ready!(fut.poll(cx).map_err(|e| e.into())))
- }
- }
- }
-}
-
impl Responder for InternalError
where
T: std::fmt::Debug + std::fmt::Display + 'static,
diff --git a/src/rmap.rs b/src/rmap.rs
index 05c1f3f15..6827a11b2 100644
--- a/src/rmap.rs
+++ b/src/rmap.rs
@@ -86,7 +86,7 @@ impl ResourceMap {
if let Some(plen) = pattern.is_prefix_match(path) {
return rmap.has_resource(&path[plen..]);
}
- } else if pattern.is_match(path) {
+ } else if pattern.is_match(path) || pattern.pattern() == "" && path == "/" {
return true;
}
}
diff --git a/src/route.rs b/src/route.rs
index e9e9d1f5d..45efd9e3c 100644
--- a/src/route.rs
+++ b/src/route.rs
@@ -16,24 +16,24 @@ use crate::responder::Responder;
use crate::service::{ServiceRequest, ServiceResponse};
use crate::HttpResponse;
-type BoxedRouteService = Box<
+type BoxedRouteService = Box<
dyn Service<
- Request = Req,
- Response = Res,
+ Request = ServiceRequest,
+ Response = ServiceResponse,
Error = Error,
- Future = LocalBoxFuture<'static, Result>,
+ Future = LocalBoxFuture<'static, Result>,
>,
>;
-type BoxedRouteNewService = Box<
+type BoxedRouteNewService = Box<
dyn ServiceFactory<
Config = (),
- Request = Req,
- Response = Res,
+ Request = ServiceRequest,
+ Response = ServiceResponse,
Error = Error,
InitError = (),
- Service = BoxedRouteService,
- Future = LocalBoxFuture<'static, Result, ()>>,
+ Service = BoxedRouteService,
+ Future = LocalBoxFuture<'static, Result>,
>,
>;
@@ -42,7 +42,7 @@ type BoxedRouteNewService = Box<
/// Route uses builder-like pattern for configuration.
/// If handler is not explicitly set, default *404 Not Found* handler is used.
pub struct Route {
- service: BoxedRouteNewService,
+ service: BoxedRouteNewService,
guards: Rc>>,
}
@@ -80,15 +80,8 @@ impl ServiceFactory for Route {
}
}
-type RouteFuture = LocalBoxFuture<
- 'static,
- Result, ()>,
->;
-
-#[pin_project::pin_project]
pub struct CreateRouteService {
- #[pin]
- fut: RouteFuture,
+ fut: LocalBoxFuture<'static, Result>,
guards: Rc>>,
}
@@ -96,9 +89,9 @@ impl Future for CreateRouteService {
type Output = Result;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
- let this = self.project();
+ let this = self.get_mut();
- match this.fut.poll(cx)? {
+ match this.fut.as_mut().poll(cx)? {
Poll::Ready(service) => Poll::Ready(Ok(RouteService {
service,
guards: this.guards.clone(),
@@ -109,7 +102,7 @@ impl Future for CreateRouteService {
}
pub struct RouteService {
- service: BoxedRouteService,
+ service: BoxedRouteService,
guards: Rc>>,
}
@@ -135,7 +128,7 @@ impl Service for RouteService {
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
- self.service.call(req).boxed_local()
+ self.service.call(req)
}
}
@@ -275,12 +268,12 @@ where
T::Service: 'static,
::Future: 'static,
{
- type Config = ();
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = Error;
+ type Config = ();
+ type Service = BoxedRouteService;
type InitError = ();
- type Service = BoxedRouteService;
type Future = LocalBoxFuture<'static, Result>;
fn new_service(&self, _: ()) -> Self::Future {
@@ -288,8 +281,7 @@ where
.new_service(())
.map(|result| match result {
Ok(service) => {
- let service: BoxedRouteService<_, _> =
- Box::new(RouteServiceWrapper { service });
+ let service = Box::new(RouteServiceWrapper { service }) as _;
Ok(service)
}
Err(_) => Err(()),
diff --git a/src/test.rs b/src/test.rs
index ee51b71ee..cff6c3e51 100644
--- a/src/test.rs
+++ b/src/test.rs
@@ -269,8 +269,9 @@ where
{
let body = read_body(res).await;
- serde_json::from_slice(&body)
- .unwrap_or_else(|_| panic!("read_response_json failed during deserialization"))
+ serde_json::from_slice(&body).unwrap_or_else(|e| {
+ panic!("read_response_json failed during deserialization: {}", e)
+ })
}
pub async fn load_stream(mut stream: S) -> Result
diff --git a/src/types/either.rs b/src/types/either.rs
new file mode 100644
index 000000000..9f1d81a0b
--- /dev/null
+++ b/src/types/either.rs
@@ -0,0 +1,274 @@
+use std::{
+ future::Future,
+ pin::Pin,
+ task::{Context, Poll},
+};
+
+use actix_http::{Error, Response};
+use bytes::Bytes;
+use futures_util::{future::LocalBoxFuture, ready, FutureExt, TryFutureExt};
+use pin_project::pin_project;
+
+use crate::{dev, request::HttpRequest, FromRequest, Responder};
+
+/// Combines two different responder types into a single type
+///
+/// ```rust
+/// use actix_web::{Either, Error, HttpResponse};
+///
+/// type RegisterResult = Either>;
+///
+/// fn index() -> RegisterResult {
+/// if is_a_variant() {
+/// // <- choose left variant
+/// Either::A(HttpResponse::BadRequest().body("Bad data"))
+/// } else {
+/// Either::B(
+/// // <- Right variant
+/// Ok(HttpResponse::Ok()
+/// .content_type("text/html")
+/// .body("Hello!"))
+/// )
+/// }
+/// }
+/// # fn is_a_variant() -> bool { true }
+/// # fn main() {}
+/// ```
+#[derive(Debug, PartialEq)]
+pub enum Either {
+ /// First branch of the type
+ A(A),
+ /// Second branch of the type
+ B(B),
+}
+
+#[cfg(test)]
+impl Either {
+ pub(self) fn unwrap_left(self) -> A {
+ match self {
+ Either::A(data) => data,
+ Either::B(_) => {
+ panic!("Cannot unwrap left branch. Either contains a right branch.")
+ }
+ }
+ }
+
+ pub(self) fn unwrap_right(self) -> B {
+ match self {
+ Either::A(_) => {
+ panic!("Cannot unwrap right branch. Either contains a left branch.")
+ }
+ Either::B(data) => data,
+ }
+ }
+}
+
+impl Responder for Either
+where
+ A: Responder,
+ B: Responder,
+{
+ type Error = Error;
+ type Future = EitherResponder;
+
+ fn respond_to(self, req: &HttpRequest) -> Self::Future {
+ match self {
+ Either::A(a) => EitherResponder::A(a.respond_to(req)),
+ Either::B(b) => EitherResponder::B(b.respond_to(req)),
+ }
+ }
+}
+
+#[pin_project(project = EitherResponderProj)]
+pub enum EitherResponder
+where
+ A: Responder,
+ B: Responder,
+{
+ A(#[pin] A::Future),
+ B(#[pin] B::Future),
+}
+
+impl Future for EitherResponder
+where
+ A: Responder,
+ B: Responder,
+{
+ type Output = Result;
+
+ fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
+ match self.project() {
+ EitherResponderProj::A(fut) => {
+ Poll::Ready(ready!(fut.poll(cx)).map_err(|e| e.into()))
+ }
+ EitherResponderProj::B(fut) => {
+ Poll::Ready(ready!(fut.poll(cx).map_err(|e| e.into())))
+ }
+ }
+ }
+}
+
+/// A composite error resulting from failure to extract an `Either`.
+///
+/// The implementation of `Into` will return the payload buffering error or the
+/// error from the primary extractor. To access the fallback error, use a match clause.
+#[derive(Debug)]
+pub enum EitherExtractError {
+ /// Error from payload buffering, such as exceeding payload max size limit.
+ Bytes(Error),
+
+ /// Error from primary extractor.
+ Extract(A, B),
+}
+
+impl Into for EitherExtractError
+where
+ A: Into,
+ B: Into,
+{
+ fn into(self) -> Error {
+ match self {
+ EitherExtractError::Bytes(err) => err,
+ EitherExtractError::Extract(a_err, _b_err) => a_err.into(),
+ }
+ }
+}
+
+/// Provides a mechanism for trying two extractors, a primary and a fallback. Useful for
+/// "polymorphic payloads" where, for example, a form might be JSON or URL encoded.
+///
+/// It is important to note that this extractor, by necessity, buffers the entire request payload
+/// as part of its implementation. Though, it does respect a `PayloadConfig`'s maximum size limit.
+impl FromRequest for Either
+where
+ A: FromRequest + 'static,
+ B: FromRequest + 'static,
+{
+ type Error = EitherExtractError;
+ type Future = LocalBoxFuture<'static, Result>;
+ type Config = ();
+
+ fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
+ let req2 = req.clone();
+
+ Bytes::from_request(req, payload)
+ .map_err(EitherExtractError::Bytes)
+ .and_then(|bytes| bytes_to_a_or_b(req2, bytes))
+ .boxed_local()
+ }
+}
+
+async fn bytes_to_a_or_b(
+ req: HttpRequest,
+ bytes: Bytes,
+) -> Result, EitherExtractError>
+where
+ A: FromRequest + 'static,
+ B: FromRequest + 'static,
+{
+ let fallback = bytes.clone();
+ let a_err;
+
+ let mut pl = payload_from_bytes(bytes);
+ match A::from_request(&req, &mut pl).await {
+ Ok(a_data) => return Ok(Either::A(a_data)),
+ // store A's error for returning if B also fails
+ Err(err) => a_err = err,
+ };
+
+ let mut pl = payload_from_bytes(fallback);
+ match B::from_request(&req, &mut pl).await {
+ Ok(b_data) => return Ok(Either::B(b_data)),
+ Err(b_err) => Err(EitherExtractError::Extract(a_err, b_err)),
+ }
+}
+
+fn payload_from_bytes(bytes: Bytes) -> dev::Payload {
+ let (_, mut h1_payload) = actix_http::h1::Payload::create(true);
+ h1_payload.unread_data(bytes);
+ dev::Payload::from(h1_payload)
+}
+
+#[cfg(test)]
+mod tests {
+ use serde::{Deserialize, Serialize};
+
+ use super::*;
+ use crate::{
+ test::TestRequest,
+ web::{Form, Json},
+ };
+
+ #[derive(Debug, Clone, Serialize, Deserialize)]
+ struct TestForm {
+ hello: String,
+ }
+
+ #[actix_rt::test]
+ async fn test_either_extract_first_try() {
+ let (req, mut pl) = TestRequest::default()
+ .set_form(&TestForm {
+ hello: "world".to_owned(),
+ })
+ .to_http_parts();
+
+ let form = Either::