diff --git a/.appveyor.yml b/.appveyor.yml
index 7addc8c08..2f0a4a7dd 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -1,6 +1,6 @@
environment:
global:
- PROJECT_NAME: actix
+ PROJECT_NAME: actix-web
matrix:
# Stable channel
- TARGET: i686-pc-windows-msvc
@@ -37,4 +37,5 @@ build: false
# Equivalent to Travis' `script` phase
test_script:
+ - cargo clean
- cargo test --no-default-features --features="flate2-rust"
diff --git a/.travis.yml b/.travis.yml
index 54a86aa7a..9b1bcff54 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -30,14 +30,17 @@ before_script:
script:
- |
- if [[ "$TRAVIS_RUST_VERSION" != "stable" ]]; then
+ if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then
cargo clean
- cargo test --features="alpn,tls" -- --nocapture
+ cargo check --features rust-tls
+ cargo check --features ssl
+ cargo check --features tls
+ cargo test --features="ssl,tls,rust-tls,uds" -- --nocapture
fi
- |
- if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then
+ if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin
- cargo tarpaulin --features="alpn,tls" --out Xml --no-count
+ RUST_BACKTRACE=1 cargo tarpaulin --features="ssl,tls,rust-tls" --out Xml
bash <(curl -s https://codecov.io/bash)
echo "Uploaded code coverage"
fi
@@ -45,8 +48,8 @@ script:
# Upload docs
after_success:
- |
- if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "beta" ]]; then
- cargo doc --features "alpn, tls, session" --no-deps &&
+ if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then
+ cargo doc --features "ssl,tls,rust-tls,session" --no-deps &&
echo "" > target/doc/index.html &&
git clone https://github.com/davisp/ghp-import.git &&
./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc &&
diff --git a/CHANGES.md b/CHANGES.md
index 15786fb69..6092544e9 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,16 +1,260 @@
# Changes
-## [0.7.1] - 2018-07-21
+## [0.7.15] - 2018-12-05
+
+## Changed
+
+* `ClientConnector::resolver` now accepts `Into` instead of `Addr`. It enables user to implement own resolver.
+
+* `QueryConfig` and `PathConfig` are made public.
+
+* `AsyncResult::async` is changed to `AsyncResult::future` as `async` is reserved keyword in 2018 edition.
### Added
- * Add implementation of `FromRequest` for `Option` and `Result`
+* By default, `Path` extractor now percent decode all characters. This behaviour can be disabled
+ with `PathConfig::default().disable_decoding()`
+
+
+## [0.7.14] - 2018-11-14
+
+### Added
+
+* Add method to configure custom error handler to `Query` and `Path` extractors.
+
+* Add method to configure `SameSite` option in `CookieIdentityPolicy`.
+
+* By default, `Path` extractor now percent decode all characters. This behaviour can be disabled
+ with `PathConfig::default().disable_decoding()`
+
+
+### Fixed
+
+* Fix websockets connection drop if request contains "content-length" header #567
+
+* Fix keep-alive timer reset
+
+* HttpServer now treats streaming bodies the same for HTTP/1.x protocols. #549
+
+* Set nodelay for socket #560
+
+
+## [0.7.13] - 2018-10-14
+
+### Fixed
+
+* Fixed rustls support
+
+* HttpServer not sending streamed request body on HTTP/2 requests #544
+
+
+## [0.7.12] - 2018-10-10
+
+### Changed
+
+* Set min version for actix
+
+* Set min version for actix-net
+
+
+## [0.7.11] - 2018-10-09
+
+### Fixed
+
+* Fixed 204 responses for http/2
+
+
+## [0.7.10] - 2018-10-09
+
+### Fixed
+
+* Fixed panic during graceful shutdown
+
+
+## [0.7.9] - 2018-10-09
+
+### Added
+
+* Added client shutdown timeout setting
+
+* Added slow request timeout setting
+
+* Respond with 408 response on slow request timeout #523
+
+
+### Fixed
+
+* HTTP1 decoding errors are reported to the client. #512
+
+* Correctly compose multiple allowed origins in CORS. #517
+
+* Websocket server finished() isn't called if client disconnects #511
+
+* Responses with the following codes: 100, 101, 102, 204 -- are sent without Content-Length header. #521
+
+* Correct usage of `no_http2` flag in `bind_*` methods. #519
+
+
+## [0.7.8] - 2018-09-17
+
+### Added
+
+* Use server `Keep-Alive` setting as slow request timeout #439
+
+### Changed
+
+* Use 5 seconds keep-alive timer by default.
+
+### Fixed
+
+* Fixed wrong error message for i16 type #510
+
+
+## [0.7.7] - 2018-09-11
+
+### Fixed
+
+* Fix linked list of HttpChannels #504
+
+* Fix requests to TestServer fail #508
+
+
+## [0.7.6] - 2018-09-07
+
+### Fixed
+
+* Fix system_exit in HttpServer #501
+
+* Fix parsing of route param containin regexes with repetition #500
+
+### Changes
+
+* Unhide `SessionBackend` and `SessionImpl` traits #455
+
+
+## [0.7.5] - 2018-09-04
+
+### Added
+
+* Added the ability to pass a custom `TlsConnector`.
+
+* Allow to register handlers on scope level #465
+
+
+### Fixed
+
+* Handle socket read disconnect
+
+* Handling scoped paths without leading slashes #460
+
+
+### Changed
+
+* Read client response until eof if connection header set to close #464
+
+
+## [0.7.4] - 2018-08-23
+
+### Added
+
+* Added `HttpServer::maxconn()` and `HttpServer::maxconnrate()`,
+ accept backpressure #250
+
+* Allow to customize connection handshake process via `HttpServer::listen_with()`
+ and `HttpServer::bind_with()` methods
+
+* Support making client connections via `tokio-uds`'s `UnixStream` when "uds" feature is enabled #472
+
+### Changed
+
+* It is allowed to use function with up to 10 parameters for handler with `extractor parameters`.
+ `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
+ even for handler with one parameter.
+
+* native-tls - 0.2
+
+* `Content-Disposition` is re-worked. Its parser is now more robust and handles quoted content better. See #461
+
+### Fixed
+
+* Use zlib instead of raw deflate for decoding and encoding payloads with
+ `Content-Encoding: deflate`.
+
+* Fixed headers formating for CORS Middleware Access-Control-Expose-Headers #436
+
+* Fix adding multiple response headers #446
+
+* Client includes port in HOST header when it is not default(e.g. not 80 and 443). #448
+
+* Panic during access without routing being set #452
+
+* Fixed http/2 error handling
+
+### Deprecated
+
+* `HttpServer::no_http2()` is deprecated, use `OpensslAcceptor::with_flags()` or
+ `RustlsAcceptor::with_flags()` instead
+
+* `HttpServer::listen_tls()`, `HttpServer::listen_ssl()`, `HttpServer::listen_rustls()` have been
+ deprecated in favor of `HttpServer::listen_with()` with specific `acceptor`.
+
+* `HttpServer::bind_tls()`, `HttpServer::bind_ssl()`, `HttpServer::bind_rustls()` have been
+ deprecated in favor of `HttpServer::bind_with()` with specific `acceptor`.
+
+
+## [0.7.3] - 2018-08-01
+
+### Added
+
+* Support HTTP/2 with rustls #36
+
+* Allow TestServer to open a websocket on any URL (TestServer::ws_at()) #433
+
+### Fixed
+
+* Fixed failure 0.1.2 compatibility
+
+* Do not override HOST header for client request #428
+
+* Gz streaming, use `flate2::write::GzDecoder` #228
+
+* HttpRequest::url_for is not working with scopes #429
+
+* Fixed headers' formating for CORS Middleware `Access-Control-Expose-Headers` header value to HTTP/1.1 & HTTP/2 spec-compliant format #436
+
+
+## [0.7.2] - 2018-07-26
+
+### Added
+
+* Add implementation of `FromRequest` for `Option` and `Result`
+
+* Allow to handle application prefix, i.e. allow to handle `/app` path
+ for application with `/app` prefix.
+ Check [`App::prefix()`](https://actix.rs/actix-web/actix_web/struct.App.html#method.prefix)
+ api doc.
+
+* Add `CookieSessionBackend::http_only` method to set `HttpOnly` directive of cookies
+
+### Changed
+
+* Upgrade to cookie 0.11
+
+* Removed the timestamp from the default logger middleware
+
+### Fixed
+
+* Missing response header "content-encoding" #421
+
+* Fix stream draining for http/2 connections #290
+
+
+## [0.7.1] - 2018-07-21
### Fixed
* Fixed default_resource 'not yet implemented' panic #410
-* Add `CookieSessionBackend::http_only` method to set `HttpOnly` directive of cookies
## [0.7.0] - 2018-07-21
diff --git a/Cargo.toml b/Cargo.toml
index a6b73ee55..7b8dcec35 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-web"
-version = "0.7.1"
+version = "0.7.15"
authors = ["Nikolay Kim "]
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
readme = "README.md"
@@ -17,7 +17,7 @@ exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"]
build = "build.rs"
[package.metadata.docs.rs]
-features = ["tls", "alpn", "session", "brotli", "flate2-c"]
+features = ["tls", "ssl", "rust-tls", "session", "brotli", "flate2-c"]
[badges]
travis-ci = { repository = "actix/actix-web", branch = "master" }
@@ -29,13 +29,22 @@ name = "actix_web"
path = "src/lib.rs"
[features]
-default = ["session", "brotli", "flate2-c"]
+default = ["session", "brotli", "flate2-c", "cell"]
# tls
-tls = ["native-tls", "tokio-tls"]
+tls = ["native-tls", "tokio-tls", "actix-net/tls"]
# openssl
-alpn = ["openssl", "tokio-openssl"]
+ssl = ["openssl", "tokio-openssl", "actix-net/ssl"]
+
+# deprecated, use "ssl"
+alpn = ["openssl", "tokio-openssl", "actix-net/ssl"]
+
+# rustls
+rust-tls = ["rustls", "tokio-rustls", "webpki", "webpki-roots", "actix-net/rust-tls"]
+
+# unix sockets
+uds = ["tokio-uds"]
# sessions feature, session require "ring" crate and c compiler
session = ["cookie/secure"]
@@ -49,21 +58,25 @@ flate2-c = ["flate2/miniz-sys"]
# rust backend for flate2 crate
flate2-rust = ["flate2/rust_backend"]
-[dependencies]
-actix = "0.7.0"
+cell = ["actix-net/cell"]
-base64 = "0.9"
+[dependencies]
+actix = "0.7.7"
+actix-net = "0.2.2"
+
+askama_escape = "0.1.0"
+base64 = "0.10"
bitflags = "1.0"
+failure = "^0.1.2"
h2 = "0.1"
-htmlescape = "0.3"
-http = "^0.1.5"
+http = "^0.1.8"
httparse = "1.3"
log = "0.4"
mime = "0.3"
mime_guess = "2.0.0-alpha"
num_cpus = "1.0"
percent-encoding = "1.0"
-rand = "0.5"
+rand = "0.6"
regex = "1.0"
serde = "1.0"
serde_json = "1.0"
@@ -74,13 +87,12 @@ encoding = "0.2"
language-tags = "0.2"
lazy_static = "1.0"
lazycell = "1.0.0"
-parking_lot = "0.6"
+parking_lot = "0.7"
+serde_urlencoded = "^0.5.3"
url = { version="1.7", features=["query_encoding"] }
-cookie = { version="0.10", features=["percent-encode"] }
+cookie = { version="0.11", features=["percent-encode"] }
brotli2 = { version="^0.3.2", optional = true }
-flate2 = { version="1.0", optional = true, default-features = false }
-
-failure = "=0.1.1"
+flate2 = { version="^1.0.2", optional = true, default-features = false }
# io
mio = "^0.6.13"
@@ -95,21 +107,27 @@ tokio-io = "0.1"
tokio-tcp = "0.1"
tokio-timer = "0.2"
tokio-reactor = "0.1"
+tokio-current-thread = "0.1"
# native-tls
-native-tls = { version="0.1", optional = true }
-tokio-tls = { version="0.1", optional = true }
+native-tls = { version="0.2", optional = true }
+tokio-tls = { version="0.2", optional = true }
# openssl
openssl = { version="0.10", optional = true }
tokio-openssl = { version="0.2", optional = true }
-# forked url_encoded
-itoa = "0.4"
-dtoa = "0.4"
+#rustls
+rustls = { version = "0.14", optional = true }
+tokio-rustls = { version = "0.8", optional = true }
+webpki = { version = "0.18", optional = true }
+webpki-roots = { version = "0.15", optional = true }
+
+# unix sockets
+tokio-uds = { version="0.2", optional = true }
[dev-dependencies]
-env_logger = "0.5"
+env_logger = "0.6"
serde_derive = "1.0"
[build-dependencies]
@@ -119,8 +137,3 @@ version_check = "0.1"
lto = true
opt-level = 3
codegen-units = 1
-
-[workspace]
-members = [
- "./",
-]
diff --git a/MIGRATION.md b/MIGRATION.md
index 29bf0c348..6b49e3e6a 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -1,3 +1,39 @@
+## 0.7.15
+
+* The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
+ your routes, you should use `%20`.
+
+ instead of
+
+ ```rust
+ fn main() {
+ let app = App::new().resource("/my index", |r| {
+ r.method(http::Method::GET)
+ .with(index);
+ });
+ }
+ ```
+
+ use
+
+ ```rust
+ fn main() {
+ let app = App::new().resource("/my%20index", |r| {
+ r.method(http::Method::GET)
+ .with(index);
+ });
+ }
+ ```
+
+* If you used `AsyncResult::async` you need to replace it with `AsyncResult::future`
+
+
+## 0.7.4
+
+* `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
+ even for handler with one parameter.
+
+
## 0.7
* `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
diff --git a/README.md b/README.md
index ec8c439ef..db3cc68c5 100644
--- a/README.md
+++ b/README.md
@@ -8,13 +8,13 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust.
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
* Transparent content compression/decompression (br, gzip, deflate)
* Configurable [request routing](https://actix.rs/docs/url-dispatch/)
-* Graceful server shutdown
* Multipart streams
* Static assets
* SSL support with OpenSSL or `native-tls`
-* Middlewares ([Logger,Session,CORS,CSRF,etc](https://actix.rs/docs/middleware/))
+* Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/))
* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
* Built on top of [Actix actor framework](https://github.com/actix/actix)
+* Experimental [Async/Await](https://github.com/mehcode/actix-web-async-await) support.
## Documentation & community resources
@@ -51,7 +51,7 @@ fn main() {
* [Protobuf support](https://github.com/actix/examples/tree/master/protobuf/)
* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/)
* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/)
-* [Tera](https://github.com/actix/examples/tree/master/template_tera/) /
+* [Tera](https://github.com/actix/examples/tree/master/template_tera/) /
[Askama](https://github.com/actix/examples/tree/master/template_askama/) templates
* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/)
* [r2d2](https://github.com/actix/examples/tree/master/r2d2/)
@@ -66,8 +66,6 @@ You may consider checking out
* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r16&hw=ph&test=plaintext)
-* Some basic benchmarks could be found in this [repository](https://github.com/fafhrd91/benchmarks).
-
## License
This project is licensed under either of
diff --git a/src/application.rs b/src/application.rs
index f36adf69e..d8a6cbe7b 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -12,6 +12,7 @@ use resource::Resource;
use router::{ResourceDef, Router};
use scope::Scope;
use server::{HttpHandler, HttpHandlerTask, IntoHttpHandler, Request};
+use with::WithFactory;
/// Application
pub struct HttpApplication {
@@ -134,13 +135,13 @@ where
/// instance for each thread, thus application state must be constructed
/// multiple times. If you want to share state between different
/// threads, a shared object should be used, e.g. `Arc`. Application
- /// state does not need to be `Send` and `Sync`.
+ /// state does not need to be `Send` or `Sync`.
pub fn with_state(state: S) -> App {
App {
parts: Some(ApplicationParts {
state,
prefix: "".to_owned(),
- router: Router::new(),
+ router: Router::new(ResourceDef::prefix("")),
middlewares: Vec::new(),
filters: Vec::new(),
encoding: ContentEncoding::Auto,
@@ -171,7 +172,9 @@ where
/// In the following example only requests with an `/app/` path
/// prefix get handled. Requests with path `/app/test/` would be
/// handled, while requests with the paths `/application` or
- /// `/other/...` would return `NOT FOUND`.
+ /// `/other/...` would return `NOT FOUND`. It is also possible to
+ /// handle `/app` path, to do this you can register resource for
+ /// empty string `""`
///
/// ```rust
/// # extern crate actix_web;
@@ -180,6 +183,8 @@ where
/// fn main() {
/// let app = App::new()
/// .prefix("/app")
+ /// .resource("", |r| r.f(|_| HttpResponse::Ok())) // <- handle `/app` path
+ /// .resource("/", |r| r.f(|_| HttpResponse::Ok())) // <- handle `/app/` path
/// .resource("/test", |r| {
/// r.get().f(|_| HttpResponse::Ok());
/// r.head().f(|_| HttpResponse::MethodNotAllowed());
@@ -194,6 +199,7 @@ where
if !prefix.starts_with('/') {
prefix.insert(0, '/')
}
+ parts.router.set_prefix(&prefix);
parts.prefix = prefix;
}
self
@@ -244,7 +250,7 @@ where
/// ```
pub fn route(mut self, path: &str, method: Method, f: F) -> App
where
- F: Fn(T) -> R + 'static,
+ F: WithFactory,
R: Responder + 'static,
T: FromRequest + 'static,
{
@@ -441,11 +447,8 @@ where
{
let mut path = path.trim().trim_right_matches('/').to_owned();
if !path.is_empty() && !path.starts_with('/') {
- path.insert(0, '/')
- }
- if path.len() > 1 && path.ends_with('/') {
- path.pop();
- }
+ path.insert(0, '/');
+ };
self.parts
.as_mut()
.expect("Use after finish")
@@ -770,8 +773,7 @@ mod tests {
.route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok())
.route("/test", Method::POST, |_: HttpRequest| {
HttpResponse::Created()
- })
- .finish();
+ }).finish();
let req = TestRequest::with_uri("/test").method(Method::GET).request();
let resp = app.run(req);
@@ -822,6 +824,23 @@ mod tests {
assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND);
}
+ #[test]
+ fn test_option_responder() {
+ let app = App::new()
+ .resource("/none", |r| r.f(|_| -> Option<&'static str> { None }))
+ .resource("/some", |r| r.f(|_| Some("some")))
+ .finish();
+
+ let req = TestRequest::with_uri("/none").request();
+ let resp = app.run(req);
+ assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND);
+
+ let req = TestRequest::with_uri("/some").request();
+ let resp = app.run(req);
+ assert_eq!(resp.as_msg().status(), StatusCode::OK);
+ assert_eq!(resp.as_msg().body(), &Body::Binary(Binary::Slice(b"some")));
+ }
+
#[test]
fn test_filter() {
let mut srv = TestServer::with_factory(|| {
@@ -840,19 +859,21 @@ mod tests {
}
#[test]
- fn test_option_responder() {
- let app = App::new()
- .resource("/none", |r| r.f(|_| -> Option<&'static str> { None }))
- .resource("/some", |r| r.f(|_| Some("some")))
- .finish();
+ fn test_prefix_root() {
+ let mut srv = TestServer::with_factory(|| {
+ App::new()
+ .prefix("/test")
+ .resource("/", |r| r.f(|_| HttpResponse::Ok()))
+ .resource("", |r| r.f(|_| HttpResponse::Created()))
+ });
- let req = TestRequest::with_uri("/none").request();
- let resp = app.run(req);
- assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND);
+ let request = srv.get().uri(srv.url("/test/")).finish().unwrap();
+ let response = srv.execute(request.send()).unwrap();
+ assert_eq!(response.status(), StatusCode::OK);
- let req = TestRequest::with_uri("/some").request();
- let resp = app.run(req);
- assert_eq!(resp.as_msg().status(), StatusCode::OK);
- assert_eq!(resp.as_msg().body(), &Body::Binary(Binary::Slice(b"some")));
+ let request = srv.get().uri(srv.url("/test")).finish().unwrap();
+ let response = srv.execute(request.send()).unwrap();
+ assert_eq!(response.status(), StatusCode::CREATED);
}
+
}
diff --git a/src/body.rs b/src/body.rs
index a93db1e92..5487dbba4 100644
--- a/src/body.rs
+++ b/src/body.rs
@@ -1,5 +1,6 @@
use bytes::{Bytes, BytesMut};
use futures::Stream;
+use std::borrow::Cow;
use std::sync::Arc;
use std::{fmt, mem};
@@ -194,12 +195,30 @@ impl From> for Binary {
}
}
+impl From> for Binary {
+ fn from(b: Cow<'static, [u8]>) -> Binary {
+ match b {
+ Cow::Borrowed(s) => Binary::Slice(s),
+ Cow::Owned(vec) => Binary::Bytes(Bytes::from(vec)),
+ }
+ }
+}
+
impl From for Binary {
fn from(s: String) -> Binary {
Binary::Bytes(Bytes::from(s))
}
}
+impl From> for Binary {
+ fn from(s: Cow<'static, str>) -> Binary {
+ match s {
+ Cow::Borrowed(s) => Binary::Slice(s.as_ref()),
+ Cow::Owned(s) => Binary::Bytes(Bytes::from(s)),
+ }
+ }
+}
+
impl<'a> From<&'a String> for Binary {
fn from(s: &'a String) -> Binary {
Binary::Bytes(Bytes::from(AsRef::<[u8]>::as_ref(&s)))
@@ -287,6 +306,16 @@ mod tests {
assert_eq!(Binary::from("test").as_ref(), b"test");
}
+ #[test]
+ fn test_cow_str() {
+ let cow: Cow<'static, str> = Cow::Borrowed("test");
+ assert_eq!(Binary::from(cow.clone()).len(), 4);
+ assert_eq!(Binary::from(cow.clone()).as_ref(), b"test");
+ let cow: Cow<'static, str> = Cow::Owned("test".to_owned());
+ assert_eq!(Binary::from(cow.clone()).len(), 4);
+ assert_eq!(Binary::from(cow.clone()).as_ref(), b"test");
+ }
+
#[test]
fn test_static_bytes() {
assert_eq!(Binary::from(b"test".as_ref()).len(), 4);
@@ -307,6 +336,16 @@ mod tests {
assert_eq!(Binary::from(Bytes::from("test")).as_ref(), b"test");
}
+ #[test]
+ fn test_cow_bytes() {
+ let cow: Cow<'static, [u8]> = Cow::Borrowed(b"test");
+ assert_eq!(Binary::from(cow.clone()).len(), 4);
+ assert_eq!(Binary::from(cow.clone()).as_ref(), b"test");
+ let cow: Cow<'static, [u8]> = Cow::Owned(Vec::from("test"));
+ assert_eq!(Binary::from(cow.clone()).len(), 4);
+ assert_eq!(Binary::from(cow.clone()).as_ref(), b"test");
+ }
+
#[test]
fn test_arc_string() {
let b = Arc::new("test".to_owned());
diff --git a/src/client/connector.rs b/src/client/connector.rs
index 6d391af87..f5affad37 100644
--- a/src/client/connector.rs
+++ b/src/client/connector.rs
@@ -5,7 +5,7 @@ use std::{fmt, io, mem, time};
use actix::resolver::{Connect as ResolveConnect, Resolver, ResolverError};
use actix::{
- fut, Actor, ActorFuture, ActorResponse, Addr, AsyncContext, Context,
+ fut, Actor, ActorFuture, ActorResponse, AsyncContext, Context,
ContextFutureSpawner, Handler, Message, Recipient, StreamHandler, Supervised,
SystemService, WrapFuture,
};
@@ -16,18 +16,40 @@ use http::{Error as HttpError, HttpTryFrom, Uri};
use tokio_io::{AsyncRead, AsyncWrite};
use tokio_timer::Delay;
-#[cfg(feature = "alpn")]
-use openssl::ssl::{Error as OpensslError, SslConnector, SslMethod};
-#[cfg(feature = "alpn")]
-use tokio_openssl::SslConnectorExt;
+#[cfg(any(feature = "alpn", feature = "ssl"))]
+use {
+ openssl::ssl::{Error as SslError, SslConnector, SslMethod},
+ tokio_openssl::SslConnectorExt,
+};
-#[cfg(all(feature = "tls", not(feature = "alpn")))]
-use native_tls::{Error as TlsError, TlsConnector};
-#[cfg(all(feature = "tls", not(feature = "alpn")))]
-use tokio_tls::TlsConnectorExt;
+#[cfg(all(
+ feature = "tls",
+ not(any(feature = "alpn", feature = "ssl", feature = "rust-tls"))
+))]
+use {
+ native_tls::{Error as SslError, TlsConnector as NativeTlsConnector},
+ tokio_tls::TlsConnector as SslConnector,
+};
+
+#[cfg(all(
+ feature = "rust-tls",
+ not(any(feature = "alpn", feature = "tls", feature = "ssl"))
+))]
+use {
+ rustls::ClientConfig, std::io::Error as SslError, std::sync::Arc,
+ tokio_rustls::TlsConnector as SslConnector, webpki::DNSNameRef, webpki_roots,
+};
+
+#[cfg(not(any(
+ feature = "alpn",
+ feature = "ssl",
+ feature = "tls",
+ feature = "rust-tls"
+)))]
+type SslConnector = ();
use server::IoStream;
-use {HAS_OPENSSL, HAS_TLS};
+use {HAS_OPENSSL, HAS_RUSTLS, HAS_TLS};
/// Client connector usage stats
#[derive(Default, Message)]
@@ -130,14 +152,14 @@ pub enum ClientConnectorError {
SslIsNotSupported,
/// SSL error
- #[cfg(feature = "alpn")]
+ #[cfg(any(
+ feature = "tls",
+ feature = "alpn",
+ feature = "ssl",
+ feature = "rust-tls",
+ ))]
#[fail(display = "{}", _0)]
- SslError(#[cause] OpensslError),
-
- /// SSL error
- #[cfg(all(feature = "tls", not(feature = "alpn")))]
- #[fail(display = "{}", _0)]
- SslError(#[cause] TlsError),
+ SslError(#[cause] SslError),
/// Resolver error
#[fail(display = "{}", _0)]
@@ -189,10 +211,8 @@ impl Paused {
/// `ClientConnector` type is responsible for transport layer of a
/// client connection.
pub struct ClientConnector {
- #[cfg(all(feature = "alpn"))]
+ #[allow(dead_code)]
connector: SslConnector,
- #[cfg(all(feature = "tls", not(feature = "alpn")))]
- connector: TlsConnector,
stats: ClientConnectorStats,
subscriber: Option>,
@@ -200,7 +220,7 @@ pub struct ClientConnector {
acq_tx: mpsc::UnboundedSender,
acq_rx: Option>,
- resolver: Option>,
+ resolver: Option>,
conn_lifetime: Duration,
conn_keep_alive: Duration,
limit: usize,
@@ -219,7 +239,7 @@ impl Actor for ClientConnector {
fn started(&mut self, ctx: &mut Self::Context) {
if self.resolver.is_none() {
- self.resolver = Some(Resolver::from_registry())
+ self.resolver = Some(Resolver::from_registry().recipient())
}
self.collect_periodic(ctx);
ctx.add_stream(self.acq_rx.take().unwrap());
@@ -233,63 +253,47 @@ impl SystemService for ClientConnector {}
impl Default for ClientConnector {
fn default() -> ClientConnector {
- #[cfg(all(feature = "alpn"))]
- {
- let builder = SslConnector::builder(SslMethod::tls()).unwrap();
- ClientConnector::with_connector(builder.build())
- }
- #[cfg(all(feature = "tls", not(feature = "alpn")))]
- {
- let (tx, rx) = mpsc::unbounded();
- let builder = TlsConnector::builder().unwrap();
- ClientConnector {
- stats: ClientConnectorStats::default(),
- subscriber: None,
- acq_tx: tx,
- acq_rx: Some(rx),
- resolver: None,
- connector: builder.build().unwrap(),
- conn_lifetime: Duration::from_secs(75),
- conn_keep_alive: Duration::from_secs(15),
- limit: 100,
- limit_per_host: 0,
- acquired: 0,
- acquired_per_host: HashMap::new(),
- available: HashMap::new(),
- to_close: Vec::new(),
- waiters: Some(HashMap::new()),
- wait_timeout: None,
- paused: Paused::No,
+ let connector = {
+ #[cfg(all(any(feature = "alpn", feature = "ssl")))]
+ {
+ SslConnector::builder(SslMethod::tls()).unwrap().build()
}
- }
- #[cfg(not(any(feature = "alpn", feature = "tls")))]
- {
- let (tx, rx) = mpsc::unbounded();
- ClientConnector {
- stats: ClientConnectorStats::default(),
- subscriber: None,
- acq_tx: tx,
- acq_rx: Some(rx),
- resolver: None,
- conn_lifetime: Duration::from_secs(75),
- conn_keep_alive: Duration::from_secs(15),
- limit: 100,
- limit_per_host: 0,
- acquired: 0,
- acquired_per_host: HashMap::new(),
- available: HashMap::new(),
- to_close: Vec::new(),
- waiters: Some(HashMap::new()),
- wait_timeout: None,
- paused: Paused::No,
+ #[cfg(all(
+ feature = "tls",
+ not(any(feature = "alpn", feature = "ssl", feature = "rust-tls"))
+ ))]
+ {
+ NativeTlsConnector::builder().build().unwrap().into()
}
- }
+
+ #[cfg(all(
+ feature = "rust-tls",
+ not(any(feature = "alpn", feature = "tls", feature = "ssl"))
+ ))]
+ {
+ let mut config = ClientConfig::new();
+ config
+ .root_store
+ .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
+ SslConnector::from(Arc::new(config))
+ }
+
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ #[cfg(not(any(
+ feature = "alpn", feature = "ssl", feature = "tls", feature = "rust-tls")))]
+ {
+ ()
+ }
+ };
+
+ #[cfg_attr(feature = "cargo-clippy", allow(let_unit_value))]
+ ClientConnector::with_connector_impl(connector)
}
}
impl ClientConnector {
- #[cfg(feature = "alpn")]
+ #[cfg(any(feature = "alpn", feature = "ssl"))]
/// Create `ClientConnector` actor with custom `SslConnector` instance.
///
/// By default `ClientConnector` uses very a simple SSL configuration.
@@ -302,7 +306,6 @@ impl ClientConnector {
/// # extern crate futures;
/// # use futures::{future, Future};
/// # use std::io::Write;
- /// # use std::process;
/// # use actix_web::actix::Actor;
/// extern crate openssl;
/// use actix_web::{actix, client::ClientConnector, client::Connect};
@@ -325,10 +328,112 @@ impl ClientConnector {
/// # actix::System::current().stop();
/// Ok(())
/// })
- /// );
+ /// });
/// }
/// ```
pub fn with_connector(connector: SslConnector) -> ClientConnector {
+ // keep level of indirection for docstrings matching featureflags
+ Self::with_connector_impl(connector)
+ }
+
+ #[cfg(all(
+ feature = "rust-tls",
+ not(any(feature = "alpn", feature = "ssl", feature = "tls"))
+ ))]
+ /// Create `ClientConnector` actor with custom `SslConnector` instance.
+ ///
+ /// By default `ClientConnector` uses very a simple SSL configuration.
+ /// With `with_connector` method it is possible to use a custom
+ /// `SslConnector` object.
+ ///
+ /// ```rust
+ /// # #![cfg(feature = "rust-tls")]
+ /// # extern crate actix_web;
+ /// # extern crate futures;
+ /// # use futures::{future, Future};
+ /// # use std::io::Write;
+ /// # use actix_web::actix::Actor;
+ /// extern crate rustls;
+ /// extern crate webpki_roots;
+ /// use actix_web::{actix, client::ClientConnector, client::Connect};
+ ///
+ /// use rustls::ClientConfig;
+ /// use std::sync::Arc;
+ ///
+ /// fn main() {
+ /// actix::run(|| {
+ /// // Start `ClientConnector` with custom `ClientConfig`
+ /// let mut config = ClientConfig::new();
+ /// config
+ /// .root_store
+ /// .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
+ /// let conn = ClientConnector::with_connector(config).start();
+ ///
+ /// conn.send(
+ /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host
+ /// .map_err(|_| ())
+ /// .and_then(|res| {
+ /// if let Ok(mut stream) = res {
+ /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
+ /// }
+ /// # actix::System::current().stop();
+ /// Ok(())
+ /// })
+ /// });
+ /// }
+ /// ```
+ pub fn with_connector(connector: ClientConfig) -> ClientConnector {
+ // keep level of indirection for docstrings matching featureflags
+ Self::with_connector_impl(SslConnector::from(Arc::new(connector)))
+ }
+
+ #[cfg(all(
+ feature = "tls",
+ not(any(feature = "ssl", feature = "alpn", feature = "rust-tls"))
+ ))]
+ /// Create `ClientConnector` actor with custom `SslConnector` instance.
+ ///
+ /// By default `ClientConnector` uses very a simple SSL configuration.
+ /// With `with_connector` method it is possible to use a custom
+ /// `SslConnector` object.
+ ///
+ /// ```rust
+ /// # #![cfg(feature = "tls")]
+ /// # extern crate actix_web;
+ /// # extern crate futures;
+ /// # use futures::{future, Future};
+ /// # use std::io::Write;
+ /// # use actix_web::actix::Actor;
+ /// extern crate native_tls;
+ /// extern crate webpki_roots;
+ /// use native_tls::TlsConnector;
+ /// use actix_web::{actix, client::ClientConnector, client::Connect};
+ ///
+ /// fn main() {
+ /// actix::run(|| {
+ /// let connector = TlsConnector::new().unwrap();
+ /// let conn = ClientConnector::with_connector(connector.into()).start();
+ ///
+ /// conn.send(
+ /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host
+ /// .map_err(|_| ())
+ /// .and_then(|res| {
+ /// if let Ok(mut stream) = res {
+ /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
+ /// }
+ /// # actix::System::current().stop();
+ /// Ok(())
+ /// })
+ /// });
+ /// }
+ /// ```
+ pub fn with_connector(connector: SslConnector) -> ClientConnector {
+ // keep level of indirection for docstrings matching featureflags
+ Self::with_connector_impl(connector)
+ }
+
+ #[inline]
+ fn with_connector_impl(connector: SslConnector) -> ClientConnector {
let (tx, rx) = mpsc::unbounded();
ClientConnector {
@@ -398,8 +503,10 @@ impl ClientConnector {
}
/// Use custom resolver actor
- pub fn resolver(mut self, addr: Addr) -> Self {
- self.resolver = Some(addr);
+ ///
+ /// By default actix's Resolver is used.
+ pub fn resolver>>(mut self, addr: A) -> Self {
+ self.resolver = Some(addr.into());
self
}
@@ -599,7 +706,7 @@ impl ClientConnector {
}
Acquire::Available => {
// create new connection
- self.connect_waiter(key.clone(), waiter, ctx);
+ self.connect_waiter(&key, waiter, ctx);
}
}
}
@@ -608,7 +715,8 @@ impl ClientConnector {
self.waiters = Some(act_waiters);
}
- fn connect_waiter(&mut self, key: Key, waiter: Waiter, ctx: &mut Context) {
+ fn connect_waiter(&mut self, key: &Key, waiter: Waiter, ctx: &mut Context) {
+ let key = key.clone();
let conn = AcquiredConn(key.clone(), Some(self.acq_tx.clone()));
let key2 = key.clone();
@@ -620,118 +728,164 @@ impl ClientConnector {
).map_err(move |_, act, _| {
act.release_key(&key2);
()
- })
- .and_then(move |res, act, _| {
- #[cfg(feature = "alpn")]
- match res {
- Err(err) => {
- let _ = waiter.tx.send(Err(err.into()));
- fut::Either::B(fut::err(()))
- }
- Ok(stream) => {
- act.stats.opened += 1;
- if conn.0.ssl {
- fut::Either::A(
- act.connector
- .connect_async(&key.host, stream)
- .into_actor(act)
- .then(move |res, act, _| {
- match res {
- Err(e) => {
- let _ = waiter.tx.send(Err(
- ClientConnectorError::SslError(e),
- ));
- }
- Ok(stream) => {
- let _ =
- waiter.tx.send(Ok(Connection::new(
- conn.0.clone(),
- Some(conn),
- Box::new(stream),
- )));
- }
+ }).and_then(move |res, act, _| {
+ #[cfg(any(feature = "alpn", feature = "ssl"))]
+ match res {
+ Err(err) => {
+ let _ = waiter.tx.send(Err(err.into()));
+ fut::Either::B(fut::err(()))
+ }
+ Ok(stream) => {
+ act.stats.opened += 1;
+ if conn.0.ssl {
+ fut::Either::A(
+ act.connector
+ .connect_async(&key.host, stream)
+ .into_actor(act)
+ .then(move |res, _, _| {
+ match res {
+ Err(e) => {
+ let _ = waiter.tx.send(Err(
+ ClientConnectorError::SslError(e),
+ ));
}
- fut::ok(())
- }),
- )
- } else {
- let _ = waiter.tx.send(Ok(Connection::new(
- conn.0.clone(),
- Some(conn),
- Box::new(stream),
- )));
- fut::Either::B(fut::ok(()))
- }
- }
- }
-
- #[cfg(all(feature = "tls", not(feature = "alpn")))]
- match res {
- Err(err) => {
- let _ = waiter.tx.send(Err(err.into()));
- fut::Either::B(fut::err(()))
- }
- Ok(stream) => {
- act.stats.opened += 1;
- if conn.0.ssl {
- fut::Either::A(
- act.connector
- .connect_async(&conn.0.host, stream)
- .into_actor(act)
- .then(move |res, _, _| {
- match res {
- Err(e) => {
- let _ = waiter.tx.send(Err(
- ClientConnectorError::SslError(e),
- ));
- }
- Ok(stream) => {
- let _ =
- waiter.tx.send(Ok(Connection::new(
- conn.0.clone(),
- Some(conn),
- Box::new(stream),
- )));
- }
+ Ok(stream) => {
+ let _ = waiter.tx.send(Ok(Connection::new(
+ conn.0.clone(),
+ Some(conn),
+ Box::new(stream),
+ )));
}
- fut::ok(())
- }),
- )
- } else {
- let _ = waiter.tx.send(Ok(Connection::new(
- conn.0.clone(),
- Some(conn),
- Box::new(stream),
- )));
- fut::Either::B(fut::ok(()))
- }
+ }
+ fut::ok(())
+ }),
+ )
+ } else {
+ let _ = waiter.tx.send(Ok(Connection::new(
+ conn.0.clone(),
+ Some(conn),
+ Box::new(stream),
+ )));
+ fut::Either::B(fut::ok(()))
}
}
+ }
- #[cfg(not(any(feature = "alpn", feature = "tls")))]
- match res {
- Err(err) => {
- let _ = waiter.tx.send(Err(err.into()));
- fut::err(())
- }
- Ok(stream) => {
- act.stats.opened += 1;
- if conn.0.ssl {
- let _ = waiter
- .tx
- .send(Err(ClientConnectorError::SslIsNotSupported));
- } else {
- let _ = waiter.tx.send(Ok(Connection::new(
- conn.0.clone(),
- Some(conn),
- Box::new(stream),
- )));
- };
- fut::ok(())
+ #[cfg(all(feature = "tls", not(any(feature = "alpn", feature = "ssl"))))]
+ match res {
+ Err(err) => {
+ let _ = waiter.tx.send(Err(err.into()));
+ fut::Either::B(fut::err(()))
+ }
+ Ok(stream) => {
+ act.stats.opened += 1;
+ if conn.0.ssl {
+ fut::Either::A(
+ act.connector
+ .connect(&conn.0.host, stream)
+ .into_actor(act)
+ .then(move |res, _, _| {
+ match res {
+ Err(e) => {
+ let _ = waiter.tx.send(Err(
+ ClientConnectorError::SslError(e),
+ ));
+ }
+ Ok(stream) => {
+ let _ = waiter.tx.send(Ok(Connection::new(
+ conn.0.clone(),
+ Some(conn),
+ Box::new(stream),
+ )));
+ }
+ }
+ fut::ok(())
+ }),
+ )
+ } else {
+ let _ = waiter.tx.send(Ok(Connection::new(
+ conn.0.clone(),
+ Some(conn),
+ Box::new(stream),
+ )));
+ fut::Either::B(fut::ok(()))
}
}
- })
- .spawn(ctx);
+ }
+
+ #[cfg(all(
+ feature = "rust-tls",
+ not(any(feature = "alpn", feature = "ssl", feature = "tls"))
+ ))]
+ match res {
+ Err(err) => {
+ let _ = waiter.tx.send(Err(err.into()));
+ fut::Either::B(fut::err(()))
+ }
+ Ok(stream) => {
+ act.stats.opened += 1;
+ if conn.0.ssl {
+ let host = DNSNameRef::try_from_ascii_str(&key.host).unwrap();
+ fut::Either::A(
+ act.connector
+ .connect(host, stream)
+ .into_actor(act)
+ .then(move |res, _, _| {
+ match res {
+ Err(e) => {
+ let _ = waiter.tx.send(Err(
+ ClientConnectorError::SslError(e),
+ ));
+ }
+ Ok(stream) => {
+ let _ = waiter.tx.send(Ok(Connection::new(
+ conn.0.clone(),
+ Some(conn),
+ Box::new(stream),
+ )));
+ }
+ }
+ fut::ok(())
+ }),
+ )
+ } else {
+ let _ = waiter.tx.send(Ok(Connection::new(
+ conn.0.clone(),
+ Some(conn),
+ Box::new(stream),
+ )));
+ fut::Either::B(fut::ok(()))
+ }
+ }
+ }
+
+ #[cfg(not(any(
+ feature = "alpn",
+ feature = "ssl",
+ feature = "tls",
+ feature = "rust-tls"
+ )))]
+ match res {
+ Err(err) => {
+ let _ = waiter.tx.send(Err(err.into()));
+ fut::err(())
+ }
+ Ok(stream) => {
+ act.stats.opened += 1;
+ if conn.0.ssl {
+ let _ =
+ waiter.tx.send(Err(ClientConnectorError::SslIsNotSupported));
+ } else {
+ let _ = waiter.tx.send(Ok(Connection::new(
+ conn.0.clone(),
+ Some(conn),
+ Box::new(stream),
+ )));
+ };
+ fut::ok(())
+ }
+ }
+ }).spawn(ctx);
}
}
@@ -783,12 +937,12 @@ impl Handler for ClientConnector {
};
// check ssl availability
- if proto.is_secure() && !HAS_OPENSSL && !HAS_TLS {
+ if proto.is_secure() && !HAS_OPENSSL && !HAS_TLS && !HAS_RUSTLS {
return ActorResponse::reply(Err(ClientConnectorError::SslIsNotSupported));
}
let host = uri.host().unwrap().to_owned();
- let port = uri.port().unwrap_or_else(|| proto.port());
+ let port = uri.port_part().map(|port| port.as_u16()).unwrap_or_else(|| proto.port());
let key = Key {
host,
port,
@@ -828,7 +982,7 @@ impl Handler for ClientConnector {
wait,
conn_timeout,
};
- self.connect_waiter(key.clone(), waiter, ctx);
+ self.connect_waiter(&key, waiter, ctx);
return ActorResponse::async(
rx.map_err(|_| ClientConnectorError::Disconnected)
@@ -885,7 +1039,7 @@ impl Handler for ClientConnector {
wait,
conn_timeout,
};
- self.connect_waiter(key.clone(), waiter, ctx);
+ self.connect_waiter(&key, waiter, ctx);
ActorResponse::async(
rx.map_err(|_| ClientConnectorError::Disconnected)
@@ -1089,6 +1243,10 @@ impl Connection {
}
/// Create a new connection from an IO Stream
+ ///
+ /// The stream can be a `UnixStream` if the Unix-only "uds" feature is enabled.
+ ///
+ /// See also `ClientRequestBuilder::with_connection()`.
pub fn from_stream(io: T) -> Connection {
Connection::new(Key::empty(), None, Box::new(io))
}
@@ -1122,6 +1280,11 @@ impl IoStream for Connection {
fn set_linger(&mut self, dur: Option) -> io::Result<()> {
IoStream::set_linger(&mut *self.stream, dur)
}
+
+ #[inline]
+ fn set_keepalive(&mut self, dur: Option) -> io::Result<()> {
+ IoStream::set_keepalive(&mut *self.stream, dur)
+ }
}
impl io::Read for Connection {
@@ -1147,3 +1310,31 @@ impl AsyncWrite for Connection {
self.stream.shutdown()
}
}
+
+#[cfg(feature = "tls")]
+use tokio_tls::TlsStream;
+
+#[cfg(feature = "tls")]
+/// This is temp solution untile actix-net migration
+impl IoStream for TlsStream {
+ #[inline]
+ fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> {
+ let _ = self.get_mut().shutdown();
+ Ok(())
+ }
+
+ #[inline]
+ fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> {
+ self.get_mut().get_mut().set_nodelay(nodelay)
+ }
+
+ #[inline]
+ fn set_linger(&mut self, dur: Option) -> io::Result<()> {
+ self.get_mut().get_mut().set_linger(dur)
+ }
+
+ #[inline]
+ fn set_keepalive(&mut self, dur: Option) -> io::Result<()> {
+ self.get_mut().get_mut().set_keepalive(dur)
+ }
+}
diff --git a/src/client/parser.rs b/src/client/parser.rs
index f5390cc34..92a7abe13 100644
--- a/src/client/parser.rs
+++ b/src/client/parser.rs
@@ -20,6 +20,7 @@ const MAX_HEADERS: usize = 96;
#[derive(Default)]
pub struct HttpResponseParser {
decoder: Option,
+ eof: bool, // indicate that we read payload until stream eof
}
#[derive(Debug, Fail)]
@@ -38,43 +39,42 @@ impl HttpResponseParser {
where
T: IoStream,
{
- // if buf is empty parse_message will always return NotReady, let's avoid that
- if buf.is_empty() {
+ loop {
+ // Don't call parser until we have data to parse.
+ if !buf.is_empty() {
+ match HttpResponseParser::parse_message(buf)
+ .map_err(HttpResponseParserError::Error)?
+ {
+ Async::Ready((msg, info)) => {
+ if let Some((decoder, eof)) = info {
+ self.eof = eof;
+ self.decoder = Some(decoder);
+ } else {
+ self.eof = false;
+ self.decoder = None;
+ }
+ return Ok(Async::Ready(msg));
+ }
+ Async::NotReady => {
+ if buf.len() >= MAX_BUFFER_SIZE {
+ return Err(HttpResponseParserError::Error(
+ ParseError::TooLarge,
+ ));
+ }
+ // Parser needs more data.
+ }
+ }
+ }
+ // Read some more data into the buffer for the parser.
match io.read_available(buf) {
- Ok(Async::Ready(true)) => {
+ Ok(Async::Ready((false, true))) => {
return Err(HttpResponseParserError::Disconnect)
}
- Ok(Async::Ready(false)) => (),
+ Ok(Async::Ready(_)) => (),
Ok(Async::NotReady) => return Ok(Async::NotReady),
Err(err) => return Err(HttpResponseParserError::Error(err.into())),
}
}
-
- loop {
- match HttpResponseParser::parse_message(buf)
- .map_err(HttpResponseParserError::Error)?
- {
- Async::Ready((msg, decoder)) => {
- self.decoder = decoder;
- return Ok(Async::Ready(msg));
- }
- Async::NotReady => {
- if buf.capacity() >= MAX_BUFFER_SIZE {
- return Err(HttpResponseParserError::Error(ParseError::TooLarge));
- }
- match io.read_available(buf) {
- Ok(Async::Ready(true)) => {
- return Err(HttpResponseParserError::Disconnect)
- }
- Ok(Async::Ready(false)) => (),
- Ok(Async::NotReady) => return Ok(Async::NotReady),
- Err(err) => {
- return Err(HttpResponseParserError::Error(err.into()))
- }
- }
- }
- }
- }
}
pub fn parse_payload(
@@ -87,8 +87,8 @@ impl HttpResponseParser {
loop {
// read payload
let (not_ready, stream_finished) = match io.read_available(buf) {
- Ok(Async::Ready(true)) => (false, true),
- Ok(Async::Ready(false)) => (false, false),
+ Ok(Async::Ready((_, true))) => (false, true),
+ Ok(Async::Ready((_, false))) => (false, false),
Ok(Async::NotReady) => (true, false),
Err(err) => return Err(err.into()),
};
@@ -104,7 +104,12 @@ impl HttpResponseParser {
return Ok(Async::NotReady);
}
if stream_finished {
- return Err(PayloadError::Incomplete);
+ // read untile eof?
+ if self.eof {
+ return Ok(Async::Ready(None));
+ } else {
+ return Err(PayloadError::Incomplete);
+ }
}
}
Err(err) => return Err(err.into()),
@@ -117,7 +122,7 @@ impl HttpResponseParser {
fn parse_message(
buf: &mut BytesMut,
- ) -> Poll<(ClientResponse, Option), ParseError> {
+ ) -> Poll<(ClientResponse, Option<(EncodingDecoder, bool)>), ParseError> {
// Unsafe: we read only this data only after httparse parses headers into.
// performance bump for pipeline benchmarks.
let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() };
@@ -163,12 +168,12 @@ impl HttpResponseParser {
}
let decoder = if status == StatusCode::SWITCHING_PROTOCOLS {
- Some(EncodingDecoder::eof())
+ Some((EncodingDecoder::eof(), true))
} else if let Some(len) = hdrs.get(header::CONTENT_LENGTH) {
// Content-Length
if let Ok(s) = len.to_str() {
if let Ok(len) = s.parse::() {
- Some(EncodingDecoder::length(len))
+ Some((EncodingDecoder::length(len), false))
} else {
debug!("illegal Content-Length: {:?}", len);
return Err(ParseError::Header);
@@ -179,7 +184,18 @@ impl HttpResponseParser {
}
} else if chunked(&hdrs)? {
// Chunked encoding
- Some(EncodingDecoder::chunked())
+ Some((EncodingDecoder::chunked(), false))
+ } else if let Some(value) = hdrs.get(header::CONNECTION) {
+ let close = if let Ok(s) = value.to_str() {
+ s == "close"
+ } else {
+ false
+ };
+ if close {
+ Some((EncodingDecoder::eof(), true))
+ } else {
+ None
+ }
} else {
None
};
diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs
index e5538b060..394b7a6cd 100644
--- a/src/client/pipeline.rs
+++ b/src/client/pipeline.rs
@@ -216,7 +216,7 @@ impl Future for SendRequest {
match pl.parse() {
Ok(Async::Ready(mut resp)) => {
- if self.req.method() == &Method::HEAD {
+ if self.req.method() == Method::HEAD {
pl.parser.take();
}
resp.set_pipeline(pl);
diff --git a/src/client/request.rs b/src/client/request.rs
index 650f0eeaa..71da8f74d 100644
--- a/src/client/request.rs
+++ b/src/client/request.rs
@@ -254,16 +254,16 @@ impl ClientRequest {
impl fmt::Debug for ClientRequest {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let res = writeln!(
+ writeln!(
f,
"\nClientRequest {:?} {}:{}",
self.version, self.method, self.uri
- );
- let _ = writeln!(f, " headers:");
+ )?;
+ writeln!(f, " headers:")?;
for (key, val) in self.headers.iter() {
- let _ = writeln!(f, " {:?}: {:?}", key, val);
+ writeln!(f, " {:?}: {:?}", key, val)?;
}
- res
+ Ok(())
}
}
@@ -291,10 +291,6 @@ impl ClientRequestBuilder {
fn _uri(&mut self, url: &str) -> &mut Self {
match Uri::try_from(url) {
Ok(uri) => {
- // set request host header
- if let Some(host) = uri.host() {
- self.set_header(header::HOST, host);
- }
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.uri = uri;
}
@@ -316,8 +312,7 @@ impl ClientRequestBuilder {
/// Set HTTP method of this request.
#[inline]
pub fn get_method(&mut self) -> &Method {
- let parts =
- parts(&mut self.request, &self.err).expect("cannot reuse request builder");
+ let parts = self.request.as_ref().expect("cannot reuse request builder");
&parts.method
}
@@ -630,9 +625,31 @@ impl ClientRequestBuilder {
self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate");
}
+ // set request host header
+ if let Some(parts) = parts(&mut self.request, &self.err) {
+ if let Some(host) = parts.uri.host() {
+ if !parts.headers.contains_key(header::HOST) {
+ let mut wrt = BytesMut::with_capacity(host.len() + 5).writer();
+
+ let _ = match parts.uri.port_part().map(|port| port.as_u16()) {
+ None | Some(80) | Some(443) => write!(wrt, "{}", host),
+ Some(port) => write!(wrt, "{}:{}", host, port),
+ };
+
+ match wrt.get_mut().take().freeze().try_into() {
+ Ok(value) => {
+ parts.headers.insert(header::HOST, value);
+ }
+ Err(e) => self.err = Some(e.into()),
+ }
+ }
+ }
+ }
+
+ // user agent
self.set_header_if_none(
header::USER_AGENT,
- concat!("Actix-web/", env!("CARGO_PKG_VERSION")),
+ concat!("actix-web/", env!("CARGO_PKG_VERSION")),
);
}
@@ -733,16 +750,16 @@ fn parts<'a>(
impl fmt::Debug for ClientRequestBuilder {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(ref parts) = self.request {
- let res = writeln!(
+ writeln!(
f,
"\nClientRequestBuilder {:?} {}:{}",
parts.version, parts.method, parts.uri
- );
- let _ = writeln!(f, " headers:");
+ )?;
+ writeln!(f, " headers:")?;
for (key, val) in parts.headers.iter() {
- let _ = writeln!(f, " {:?}: {:?}", key, val);
+ writeln!(f, " {:?}: {:?}", key, val)?;
}
- res
+ Ok(())
} else {
write!(f, "ClientRequestBuilder(Consumed)")
}
diff --git a/src/client/response.rs b/src/client/response.rs
index 0c094a2aa..5f1f42649 100644
--- a/src/client/response.rs
+++ b/src/client/response.rs
@@ -95,12 +95,12 @@ impl ClientResponse {
impl fmt::Debug for ClientResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let res = writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status());
- let _ = writeln!(f, " headers:");
+ writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status())?;
+ writeln!(f, " headers:")?;
for (key, val) in self.headers().iter() {
- let _ = writeln!(f, " {:?}: {:?}", key, val);
+ writeln!(f, " {:?}: {:?}", key, val)?;
}
- res
+ Ok(())
}
}
diff --git a/src/client/writer.rs b/src/client/writer.rs
index b691407dd..321753bbf 100644
--- a/src/client/writer.rs
+++ b/src/client/writer.rs
@@ -1,4 +1,7 @@
-#![cfg_attr(feature = "cargo-clippy", allow(redundant_field_names))]
+#![cfg_attr(
+ feature = "cargo-clippy",
+ allow(redundant_field_names)
+)]
use std::cell::RefCell;
use std::fmt::Write as FmtWrite;
@@ -8,7 +11,7 @@ use std::io::{self, Write};
use brotli2::write::BrotliEncoder;
use bytes::{BufMut, BytesMut};
#[cfg(feature = "flate2")]
-use flate2::write::{DeflateEncoder, GzEncoder};
+use flate2::write::{GzEncoder, ZlibEncoder};
#[cfg(feature = "flate2")]
use flate2::Compression;
use futures::{Async, Poll};
@@ -232,7 +235,7 @@ fn content_encoder(buf: BytesMut, req: &mut ClientRequest) -> Output {
let mut enc = match encoding {
#[cfg(feature = "flate2")]
ContentEncoding::Deflate => ContentEncoder::Deflate(
- DeflateEncoder::new(transfer, Compression::default()),
+ ZlibEncoder::new(transfer, Compression::default()),
),
#[cfg(feature = "flate2")]
ContentEncoding::Gzip => ContentEncoder::Gzip(GzEncoder::new(
@@ -302,10 +305,9 @@ fn content_encoder(buf: BytesMut, req: &mut ClientRequest) -> Output {
req.replace_body(body);
let enc = match encoding {
#[cfg(feature = "flate2")]
- ContentEncoding::Deflate => ContentEncoder::Deflate(DeflateEncoder::new(
- transfer,
- Compression::default(),
- )),
+ ContentEncoding::Deflate => {
+ ContentEncoder::Deflate(ZlibEncoder::new(transfer, Compression::default()))
+ }
#[cfg(feature = "flate2")]
ContentEncoding::Gzip => {
ContentEncoder::Gzip(GzEncoder::new(transfer, Compression::default()))
diff --git a/src/de.rs b/src/de.rs
index ecb2fa9ae..05f8914f8 100644
--- a/src/de.rs
+++ b/src/de.rs
@@ -1,7 +1,10 @@
+use std::rc::Rc;
+
use serde::de::{self, Deserializer, Error as DeError, Visitor};
use httprequest::HttpRequest;
use param::ParamsIter;
+use uri::RESERVED_QUOTER;
macro_rules! unsupported_type {
($trait_fn:ident, $name:expr) => {
@@ -13,6 +16,20 @@ macro_rules! unsupported_type {
};
}
+macro_rules! percent_decode_if_needed {
+ ($value:expr, $decode:expr) => {
+ if $decode {
+ if let Some(ref mut value) = RESERVED_QUOTER.requote($value.as_bytes()) {
+ Rc::make_mut(value).parse()
+ } else {
+ $value.parse()
+ }
+ } else {
+ $value.parse()
+ }
+ }
+}
+
macro_rules! parse_single_value {
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
fn $trait_fn(self, visitor: V) -> Result
@@ -23,11 +40,11 @@ macro_rules! parse_single_value {
format!("wrong number of parameters: {} expected 1",
self.req.match_info().len()).as_str()))
} else {
- let v = self.req.match_info()[0].parse().map_err(
- |_| de::value::Error::custom(
- format!("can not parse {:?} to a {}",
- &self.req.match_info()[0], $tp)))?;
- visitor.$visit_fn(v)
+ let v_parsed = percent_decode_if_needed!(&self.req.match_info()[0], self.decode)
+ .map_err(|_| de::value::Error::custom(
+ format!("can not parse {:?} to a {}", &self.req.match_info()[0], $tp)
+ ))?;
+ visitor.$visit_fn(v_parsed)
}
}
}
@@ -35,11 +52,12 @@ macro_rules! parse_single_value {
pub struct PathDeserializer<'de, S: 'de> {
req: &'de HttpRequest,
+ decode: bool,
}
impl<'de, S: 'de> PathDeserializer<'de, S> {
- pub fn new(req: &'de HttpRequest) -> Self {
- PathDeserializer { req }
+ pub fn new(req: &'de HttpRequest, decode: bool) -> Self {
+ PathDeserializer { req, decode }
}
}
@@ -53,6 +71,7 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> {
visitor.visit_map(ParamsDeserializer {
params: self.req.match_info().iter(),
current: None,
+ decode: self.decode,
})
}
@@ -107,6 +126,7 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> {
} else {
visitor.visit_seq(ParamsSeq {
params: self.req.match_info().iter(),
+ decode: self.decode,
})
}
}
@@ -128,6 +148,7 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> {
} else {
visitor.visit_seq(ParamsSeq {
params: self.req.match_info().iter(),
+ decode: self.decode,
})
}
}
@@ -141,28 +162,13 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> {
Err(de::value::Error::custom("unsupported type: enum"))
}
- fn deserialize_str(self, visitor: V) -> Result
- where
- V: Visitor<'de>,
- {
- if self.req.match_info().len() != 1 {
- Err(de::value::Error::custom(
- format!(
- "wrong number of parameters: {} expected 1",
- self.req.match_info().len()
- ).as_str(),
- ))
- } else {
- visitor.visit_str(&self.req.match_info()[0])
- }
- }
-
fn deserialize_seq(self, visitor: V) -> Result
where
V: Visitor<'de>,
{
visitor.visit_seq(ParamsSeq {
params: self.req.match_info().iter(),
+ decode: self.decode,
})
}
@@ -175,7 +181,7 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> {
parse_single_value!(deserialize_bool, visit_bool, "bool");
parse_single_value!(deserialize_i8, visit_i8, "i8");
parse_single_value!(deserialize_i16, visit_i16, "i16");
- parse_single_value!(deserialize_i32, visit_i32, "i16");
+ parse_single_value!(deserialize_i32, visit_i32, "i32");
parse_single_value!(deserialize_i64, visit_i64, "i64");
parse_single_value!(deserialize_u8, visit_u8, "u8");
parse_single_value!(deserialize_u16, visit_u16, "u16");
@@ -184,13 +190,16 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> {
parse_single_value!(deserialize_f32, visit_f32, "f32");
parse_single_value!(deserialize_f64, visit_f64, "f64");
parse_single_value!(deserialize_string, visit_string, "String");
+ parse_single_value!(deserialize_str, visit_string, "String");
parse_single_value!(deserialize_byte_buf, visit_string, "String");
parse_single_value!(deserialize_char, visit_char, "char");
+
}
struct ParamsDeserializer<'de> {
params: ParamsIter<'de>,
current: Option<(&'de str, &'de str)>,
+ decode: bool,
}
impl<'de> de::MapAccess<'de> for ParamsDeserializer<'de> {
@@ -212,7 +221,7 @@ impl<'de> de::MapAccess<'de> for ParamsDeserializer<'de> {
V: de::DeserializeSeed<'de>,
{
if let Some((_, value)) = self.current.take() {
- seed.deserialize(Value { value })
+ seed.deserialize(Value { value, decode: self.decode })
} else {
Err(de::value::Error::custom("unexpected item"))
}
@@ -252,16 +261,18 @@ macro_rules! parse_value {
fn $trait_fn(self, visitor: V) -> Result
where V: Visitor<'de>
{
- let v = self.value.parse().map_err(
- |_| de::value::Error::custom(
- format!("can not parse {:?} to a {}", self.value, $tp)))?;
- visitor.$visit_fn(v)
+ let v_parsed = percent_decode_if_needed!(&self.value, self.decode)
+ .map_err(|_| de::value::Error::custom(
+ format!("can not parse {:?} to a {}", &self.value, $tp)
+ ))?;
+ visitor.$visit_fn(v_parsed)
}
}
}
struct Value<'de> {
value: &'de str,
+ decode: bool,
}
impl<'de> Deserializer<'de> for Value<'de> {
@@ -377,6 +388,7 @@ impl<'de> Deserializer<'de> for Value<'de> {
struct ParamsSeq<'de> {
params: ParamsIter<'de>,
+ decode: bool,
}
impl<'de> de::SeqAccess<'de> for ParamsSeq<'de> {
@@ -387,7 +399,7 @@ impl<'de> de::SeqAccess<'de> for ParamsSeq<'de> {
T: de::DeserializeSeed<'de>,
{
match self.params.next() {
- Some(item) => Ok(Some(seed.deserialize(Value { value: item.1 })?)),
+ Some(item) => Ok(Some(seed.deserialize(Value { value: item.1, decode: self.decode })?)),
None => Ok(None),
}
}
diff --git a/src/error.rs b/src/error.rs
index 461b23e20..1766c1523 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -52,7 +52,8 @@ pub struct Error {
impl Error {
/// Deprecated way to reference the underlying response error.
#[deprecated(
- since = "0.6.0", note = "please use `Error::as_response_error()` instead"
+ since = "0.6.0",
+ note = "please use `Error::as_response_error()` instead"
)]
pub fn cause(&self) -> &ResponseError {
self.cause.as_ref()
@@ -97,21 +98,9 @@ impl Error {
//
// So we first downcast into that compat, to then further downcast through
// the failure's Error downcasting system into the original failure.
- //
- // This currently requires a transmute. This could be avoided if failure
- // provides a deref: https://github.com/rust-lang-nursery/failure/pull/213
let compat: Option<&failure::Compat> =
Fail::downcast_ref(self.cause.as_fail());
- if let Some(compat) = compat {
- pub struct CompatWrappedError {
- error: failure::Error,
- }
- let compat: &CompatWrappedError =
- unsafe { &*(compat as *const _ as *const CompatWrappedError) };
- compat.error.downcast_ref()
- } else {
- None
- }
+ compat.and_then(|e| e.get_ref().downcast_ref())
}
}
@@ -770,6 +759,16 @@ where
InternalError::new(err, StatusCode::UNAUTHORIZED).into()
}
+/// Helper function that creates wrapper of any error and generate
+/// *PAYMENT_REQUIRED* response.
+#[allow(non_snake_case)]
+pub fn ErrorPaymentRequired(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::PAYMENT_REQUIRED).into()
+}
+
/// Helper function that creates wrapper of any error and generate *FORBIDDEN*
/// response.
#[allow(non_snake_case)]
@@ -800,6 +799,26 @@ where
InternalError::new(err, StatusCode::METHOD_NOT_ALLOWED).into()
}
+/// Helper function that creates wrapper of any error and generate *NOT
+/// ACCEPTABLE* response.
+#[allow(non_snake_case)]
+pub fn ErrorNotAcceptable(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::NOT_ACCEPTABLE).into()
+}
+
+/// Helper function that creates wrapper of any error and generate *PROXY
+/// AUTHENTICATION REQUIRED* response.
+#[allow(non_snake_case)]
+pub fn ErrorProxyAuthenticationRequired(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::PROXY_AUTHENTICATION_REQUIRED).into()
+}
+
/// Helper function that creates wrapper of any error and generate *REQUEST
/// TIMEOUT* response.
#[allow(non_snake_case)]
@@ -830,6 +849,16 @@ where
InternalError::new(err, StatusCode::GONE).into()
}
+/// Helper function that creates wrapper of any error and generate *LENGTH
+/// REQUIRED* response.
+#[allow(non_snake_case)]
+pub fn ErrorLengthRequired(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::LENGTH_REQUIRED).into()
+}
+
/// Helper function that creates wrapper of any error and generate
/// *PRECONDITION FAILED* response.
#[allow(non_snake_case)]
@@ -840,6 +869,46 @@ where
InternalError::new(err, StatusCode::PRECONDITION_FAILED).into()
}
+/// Helper function that creates wrapper of any error and generate
+/// *PAYLOAD TOO LARGE* response.
+#[allow(non_snake_case)]
+pub fn ErrorPayloadTooLarge(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::PAYLOAD_TOO_LARGE).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *URI TOO LONG* response.
+#[allow(non_snake_case)]
+pub fn ErrorUriTooLong(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::URI_TOO_LONG).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *UNSUPPORTED MEDIA TYPE* response.
+#[allow(non_snake_case)]
+pub fn ErrorUnsupportedMediaType(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::UNSUPPORTED_MEDIA_TYPE).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *RANGE NOT SATISFIABLE* response.
+#[allow(non_snake_case)]
+pub fn ErrorRangeNotSatisfiable(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::RANGE_NOT_SATISFIABLE).into()
+}
+
/// Helper function that creates wrapper of any error and generate
/// *EXPECTATION FAILED* response.
#[allow(non_snake_case)]
@@ -850,6 +919,106 @@ where
InternalError::new(err, StatusCode::EXPECTATION_FAILED).into()
}
+/// Helper function that creates wrapper of any error and generate
+/// *IM A TEAPOT* response.
+#[allow(non_snake_case)]
+pub fn ErrorImATeapot(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::IM_A_TEAPOT).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *MISDIRECTED REQUEST* response.
+#[allow(non_snake_case)]
+pub fn ErrorMisdirectedRequest(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::MISDIRECTED_REQUEST).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *UNPROCESSABLE ENTITY* response.
+#[allow(non_snake_case)]
+pub fn ErrorUnprocessableEntity(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::UNPROCESSABLE_ENTITY).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *LOCKED* response.
+#[allow(non_snake_case)]
+pub fn ErrorLocked(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::LOCKED).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *FAILED DEPENDENCY* response.
+#[allow(non_snake_case)]
+pub fn ErrorFailedDependency(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::FAILED_DEPENDENCY).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *UPGRADE REQUIRED* response.
+#[allow(non_snake_case)]
+pub fn ErrorUpgradeRequired(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::UPGRADE_REQUIRED).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *PRECONDITION REQUIRED* response.
+#[allow(non_snake_case)]
+pub fn ErrorPreconditionRequired(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::PRECONDITION_REQUIRED).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *TOO MANY REQUESTS* response.
+#[allow(non_snake_case)]
+pub fn ErrorTooManyRequests(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::TOO_MANY_REQUESTS).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *REQUEST HEADER FIELDS TOO LARGE* response.
+#[allow(non_snake_case)]
+pub fn ErrorRequestHeaderFieldsTooLarge(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE).into()
+}
+
+/// Helper function that creates wrapper of any error and generate
+/// *UNAVAILABLE FOR LEGAL REASONS* response.
+#[allow(non_snake_case)]
+pub fn ErrorUnavailableForLegalReasons(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS).into()
+}
+
/// Helper function that creates wrapper of any error and
/// generate *INTERNAL SERVER ERROR* response.
#[allow(non_snake_case)]
@@ -900,6 +1069,66 @@ where
InternalError::new(err, StatusCode::GATEWAY_TIMEOUT).into()
}
+/// Helper function that creates wrapper of any error and
+/// generate *HTTP VERSION NOT SUPPORTED* response.
+#[allow(non_snake_case)]
+pub fn ErrorHttpVersionNotSupported(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::HTTP_VERSION_NOT_SUPPORTED).into()
+}
+
+/// Helper function that creates wrapper of any error and
+/// generate *VARIANT ALSO NEGOTIATES* response.
+#[allow(non_snake_case)]
+pub fn ErrorVariantAlsoNegotiates(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::VARIANT_ALSO_NEGOTIATES).into()
+}
+
+/// Helper function that creates wrapper of any error and
+/// generate *INSUFFICIENT STORAGE* response.
+#[allow(non_snake_case)]
+pub fn ErrorInsufficientStorage(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::INSUFFICIENT_STORAGE).into()
+}
+
+/// Helper function that creates wrapper of any error and
+/// generate *LOOP DETECTED* response.
+#[allow(non_snake_case)]
+pub fn ErrorLoopDetected(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::LOOP_DETECTED).into()
+}
+
+/// Helper function that creates wrapper of any error and
+/// generate *NOT EXTENDED* response.
+#[allow(non_snake_case)]
+pub fn ErrorNotExtended(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::NOT_EXTENDED).into()
+}
+
+/// Helper function that creates wrapper of any error and
+/// generate *NETWORK AUTHENTICATION REQUIRED* response.
+#[allow(non_snake_case)]
+pub fn ErrorNetworkAuthenticationRequired(err: T) -> Error
+where
+ T: Send + Sync + fmt::Debug + fmt::Display + 'static,
+{
+ InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into()
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1079,6 +1308,9 @@ mod tests {
let r: HttpResponse = ErrorUnauthorized("err").into();
assert_eq!(r.status(), StatusCode::UNAUTHORIZED);
+ let r: HttpResponse = ErrorPaymentRequired("err").into();
+ assert_eq!(r.status(), StatusCode::PAYMENT_REQUIRED);
+
let r: HttpResponse = ErrorForbidden("err").into();
assert_eq!(r.status(), StatusCode::FORBIDDEN);
@@ -1088,6 +1320,12 @@ mod tests {
let r: HttpResponse = ErrorMethodNotAllowed("err").into();
assert_eq!(r.status(), StatusCode::METHOD_NOT_ALLOWED);
+ let r: HttpResponse = ErrorNotAcceptable("err").into();
+ assert_eq!(r.status(), StatusCode::NOT_ACCEPTABLE);
+
+ let r: HttpResponse = ErrorProxyAuthenticationRequired("err").into();
+ assert_eq!(r.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED);
+
let r: HttpResponse = ErrorRequestTimeout("err").into();
assert_eq!(r.status(), StatusCode::REQUEST_TIMEOUT);
@@ -1097,12 +1335,57 @@ mod tests {
let r: HttpResponse = ErrorGone("err").into();
assert_eq!(r.status(), StatusCode::GONE);
+ let r: HttpResponse = ErrorLengthRequired("err").into();
+ assert_eq!(r.status(), StatusCode::LENGTH_REQUIRED);
+
let r: HttpResponse = ErrorPreconditionFailed("err").into();
assert_eq!(r.status(), StatusCode::PRECONDITION_FAILED);
+ let r: HttpResponse = ErrorPayloadTooLarge("err").into();
+ assert_eq!(r.status(), StatusCode::PAYLOAD_TOO_LARGE);
+
+ let r: HttpResponse = ErrorUriTooLong("err").into();
+ assert_eq!(r.status(), StatusCode::URI_TOO_LONG);
+
+ let r: HttpResponse = ErrorUnsupportedMediaType("err").into();
+ assert_eq!(r.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
+
+ let r: HttpResponse = ErrorRangeNotSatisfiable("err").into();
+ assert_eq!(r.status(), StatusCode::RANGE_NOT_SATISFIABLE);
+
let r: HttpResponse = ErrorExpectationFailed("err").into();
assert_eq!(r.status(), StatusCode::EXPECTATION_FAILED);
+ let r: HttpResponse = ErrorImATeapot("err").into();
+ assert_eq!(r.status(), StatusCode::IM_A_TEAPOT);
+
+ let r: HttpResponse = ErrorMisdirectedRequest("err").into();
+ assert_eq!(r.status(), StatusCode::MISDIRECTED_REQUEST);
+
+ let r: HttpResponse = ErrorUnprocessableEntity("err").into();
+ assert_eq!(r.status(), StatusCode::UNPROCESSABLE_ENTITY);
+
+ let r: HttpResponse = ErrorLocked("err").into();
+ assert_eq!(r.status(), StatusCode::LOCKED);
+
+ let r: HttpResponse = ErrorFailedDependency("err").into();
+ assert_eq!(r.status(), StatusCode::FAILED_DEPENDENCY);
+
+ let r: HttpResponse = ErrorUpgradeRequired("err").into();
+ assert_eq!(r.status(), StatusCode::UPGRADE_REQUIRED);
+
+ let r: HttpResponse = ErrorPreconditionRequired("err").into();
+ assert_eq!(r.status(), StatusCode::PRECONDITION_REQUIRED);
+
+ let r: HttpResponse = ErrorTooManyRequests("err").into();
+ assert_eq!(r.status(), StatusCode::TOO_MANY_REQUESTS);
+
+ let r: HttpResponse = ErrorRequestHeaderFieldsTooLarge("err").into();
+ assert_eq!(r.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
+
+ let r: HttpResponse = ErrorUnavailableForLegalReasons("err").into();
+ assert_eq!(r.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);
+
let r: HttpResponse = ErrorInternalServerError("err").into();
assert_eq!(r.status(), StatusCode::INTERNAL_SERVER_ERROR);
@@ -1117,5 +1400,23 @@ mod tests {
let r: HttpResponse = ErrorGatewayTimeout("err").into();
assert_eq!(r.status(), StatusCode::GATEWAY_TIMEOUT);
+
+ let r: HttpResponse = ErrorHttpVersionNotSupported("err").into();
+ assert_eq!(r.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
+
+ let r: HttpResponse = ErrorVariantAlsoNegotiates("err").into();
+ assert_eq!(r.status(), StatusCode::VARIANT_ALSO_NEGOTIATES);
+
+ let r: HttpResponse = ErrorInsufficientStorage("err").into();
+ assert_eq!(r.status(), StatusCode::INSUFFICIENT_STORAGE);
+
+ let r: HttpResponse = ErrorLoopDetected("err").into();
+ assert_eq!(r.status(), StatusCode::LOOP_DETECTED);
+
+ let r: HttpResponse = ErrorNotExtended("err").into();
+ assert_eq!(r.status(), StatusCode::NOT_EXTENDED);
+
+ let r: HttpResponse = ErrorNetworkAuthenticationRequired("err").into();
+ assert_eq!(r.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED);
}
}
diff --git a/src/extensions.rs b/src/extensions.rs
index da7b5ba24..430b87bda 100644
--- a/src/extensions.rs
+++ b/src/extensions.rs
@@ -31,6 +31,7 @@ impl Hasher for IdHasher {
type AnyMap = HashMap, BuildHasherDefault>;
+#[derive(Default)]
/// A type map of request extensions.
pub struct Extensions {
map: AnyMap,
@@ -39,7 +40,7 @@ pub struct Extensions {
impl Extensions {
/// Create an empty `Extensions`.
#[inline]
- pub(crate) fn new() -> Extensions {
+ pub fn new() -> Extensions {
Extensions {
map: HashMap::default(),
}
diff --git a/src/extractor.rs b/src/extractor.rs
index 5b3a69a89..861334f32 100644
--- a/src/extractor.rs
+++ b/src/extractor.rs
@@ -6,7 +6,7 @@ use std::{fmt, str};
use bytes::Bytes;
use encoding::all::UTF_8;
use encoding::types::{DecoderTrap, Encoding};
-use futures::{Async, Future, Poll, future};
+use futures::{future, Async, Future, Poll};
use mime::Mime;
use serde::de::{self, DeserializeOwned};
use serde_urlencoded;
@@ -19,7 +19,8 @@ use httprequest::HttpRequest;
use Either;
#[derive(PartialEq, Eq, PartialOrd, Ord)]
-/// Extract typed information from the request's path.
+/// Extract typed information from the request's path. Information from the path is
+/// URL decoded. Decoding of special characters can be disabled through `PathConfig`.
///
/// ## Example
///
@@ -102,22 +103,83 @@ impl Path {
}
}
+impl From for Path {
+ fn from(inner: T) -> Path {
+ Path { inner }
+ }
+}
+
impl FromRequest for Path
where
T: DeserializeOwned,
{
- type Config = ();
+ type Config = PathConfig;
type Result = Result;
#[inline]
- fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result {
+ fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result {
let req = req.clone();
- de::Deserialize::deserialize(PathDeserializer::new(&req))
- .map_err(ErrorNotFound)
+ let req2 = req.clone();
+ let err = Rc::clone(&cfg.ehandler);
+ de::Deserialize::deserialize(PathDeserializer::new(&req, cfg.decode))
+ .map_err(move |e| (*err)(e, &req2))
.map(|inner| Path { inner })
}
}
+/// Path extractor configuration
+///
+/// ```rust
+/// # extern crate actix_web;
+/// use actix_web::{error, http, App, HttpResponse, Path, Result};
+///
+/// /// deserialize `Info` from request's body, max payload size is 4kb
+/// fn index(info: Path<(u32, String)>) -> Result {
+/// Ok(format!("Welcome {}!", info.1))
+/// }
+///
+/// fn main() {
+/// let app = App::new().resource("/index.html/{id}/{name}", |r| {
+/// r.method(http::Method::GET).with_config(index, |cfg| {
+/// cfg.0.error_handler(|err, req| {
+/// // <- create custom error response
+/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into()
+/// });
+/// })
+/// });
+/// }
+/// ```
+pub struct PathConfig {
+ ehandler: Rc) -> Error>,
+ decode: bool,
+}
+impl PathConfig {
+ /// Set custom error handler
+ pub fn error_handler(&mut self, f: F) -> &mut Self
+ where
+ F: Fn(serde_urlencoded::de::Error, &HttpRequest) -> Error + 'static,
+ {
+ self.ehandler = Rc::new(f);
+ self
+ }
+
+ /// Disable decoding of URL encoded special charaters from the path
+ pub fn disable_decoding(&mut self) -> &mut Self
+ {
+ self.decode = false;
+ self
+ }
+}
+
+impl Default for PathConfig {
+ fn default() -> Self {
+ PathConfig {
+ ehandler: Rc::new(|e, _| ErrorNotFound(e)),
+ decode: true,
+ }
+ }
+}
+
impl fmt::Debug for Path {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.inner.fmt(f)
@@ -195,17 +257,69 @@ impl FromRequest for Query
where
T: de::DeserializeOwned,
{
- type Config = ();
+ type Config = QueryConfig;
type Result = Result;
#[inline]
- fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result {
+ fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result {
+ let req2 = req.clone();
+ let err = Rc::clone(&cfg.ehandler);
serde_urlencoded::from_str::(req.query_string())
- .map_err(|e| e.into())
+ .map_err(move |e| (*err)(e, &req2))
.map(Query)
}
}
+/// Query extractor configuration
+///
+/// ```rust
+/// # extern crate actix_web;
+/// #[macro_use] extern crate serde_derive;
+/// use actix_web::{error, http, App, HttpResponse, Query, Result};
+///
+/// #[derive(Deserialize)]
+/// struct Info {
+/// username: String,
+/// }
+///
+/// /// deserialize `Info` from request's body, max payload size is 4kb
+/// fn index(info: Query) -> Result {
+/// Ok(format!("Welcome {}!", info.username))
+/// }
+///
+/// fn main() {
+/// let app = App::new().resource("/index.html", |r| {
+/// r.method(http::Method::GET).with_config(index, |cfg| {
+/// cfg.0.error_handler(|err, req| {
+/// // <- create custom error response
+/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into()
+/// });
+/// })
+/// });
+/// }
+/// ```
+pub struct QueryConfig {
+ ehandler: Rc) -> Error>,
+}
+impl QueryConfig {
+ /// Set custom error handler
+ pub fn error_handler(&mut self, f: F) -> &mut Self
+ where
+ F: Fn(serde_urlencoded::de::Error, &HttpRequest) -> Error + 'static,
+ {
+ self.ehandler = Rc::new(f);
+ self
+ }
+}
+
+impl Default for QueryConfig {
+ fn default() -> Self {
+ QueryConfig {
+ ehandler: Rc::new(|e, _| e.into()),
+ }
+ }
+}
+
impl fmt::Debug for Query {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
@@ -327,7 +441,7 @@ impl fmt::Display for Form {
/// |r| {
/// r.method(http::Method::GET)
/// // register form handler and change form extractor configuration
-/// .with_config(index, |cfg| {cfg.limit(4096);})
+/// .with_config(index, |cfg| {cfg.0.limit(4096);})
/// },
/// );
/// }
@@ -422,7 +536,7 @@ impl FromRequest for Bytes {
/// let app = App::new().resource("/index.html", |r| {
/// r.method(http::Method::GET)
/// .with_config(index, |cfg| { // <- register handler with extractor params
-/// cfg.limit(4096); // <- limit size of the payload
+/// cfg.0.limit(4096); // <- limit size of the payload
/// })
/// });
/// }
@@ -505,19 +619,18 @@ impl FromRequest for String {
/// });
/// }
/// ```
-impl FromRequest for Option where T: FromRequest {
+impl FromRequest for Option
+where
+ T: FromRequest,
+{
type Config = T::Config;
type Result = Box, Error = Error>>;
#[inline]
fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result {
- Box::new(T::from_request(req, cfg).into().then( |r| {
- match r {
- Ok(v) => future::ok(Some(v)),
- Err(e) => {
- future::ok(None)
- }
- }
+ Box::new(T::from_request(req, cfg).into().then(|r| match r {
+ Ok(v) => future::ok(Some(v)),
+ Err(_) => future::ok(None),
}))
}
}
@@ -711,13 +824,16 @@ impl Default for EitherConfig where A: FromRequest, B: FromRequ
/// });
/// }
/// ```
-impl FromRequest for Result where T: FromRequest{
+impl FromRequest for Result
+where
+ T: FromRequest,
+{
type Config = T::Config;
type Result = Box, Error = Error>>;
#[inline]
fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result {
- Box::new(T::from_request(req, cfg).into().then( |r| { future::ok(r) }))
+ Box::new(T::from_request(req, cfg).into().then(future::ok))
}
}
@@ -833,6 +949,12 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => {
}
});
+impl FromRequest for () {
+ type Config = ();
+ type Result = Self;
+ fn from_request(_req: &HttpRequest, _cfg: &Self::Config) -> Self::Result {}
+}
+
tuple_from_req!(TupleFromRequest1, (0, A));
tuple_from_req!(TupleFromRequest2, (0, A), (1, B));
tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C));
@@ -938,8 +1060,8 @@ mod tests {
header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
).header(header::CONTENT_LENGTH, "11")
- .set_payload(Bytes::from_static(b"hello=world"))
- .finish();
+ .set_payload(Bytes::from_static(b"hello=world"))
+ .finish();
let mut cfg = FormConfig::default();
cfg.limit(4096);
@@ -961,7 +1083,10 @@ mod tests {
let mut cfg = FormConfig::default();
cfg.limit(4096);
- match Option::