1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-04 09:56:22 +02:00

Compare commits

...

19 Commits

Author SHA1 Message Date
156c97cef2 prepare awc release 2.0.1 2020-10-30 02:50:53 +00:00
798d744eef prepare http release 2.1.0 2020-10-30 02:19:56 +00:00
4cb833616a deprecate builder if-x methods (#1760) 2020-10-30 02:10:05 +00:00
9963a5ef54 expose on_connect v2 (#1754)
Co-authored-by: Mikail Bagishov <bagishov.mikail@yandex.ru>
2020-10-30 02:03:26 +00:00
4519db36b2 register fns for custom request-derived logging units (#1749)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-29 18:38:49 +00:00
7030bf5fe8 Adding app_data to ServiceConfig (#1758)
Co-authored-by: Rob Ede <robjtede@icloud.com>
Co-authored-by: Augusto <augusto@flowciety.de>
2020-10-26 17:02:45 +00:00
20078fe603 Bump pin-project to 1.0 (#1733) 2020-10-25 19:41:44 +09:00
06e5042b94 use idenity encoding on client if no compression features are enabled (#1737)
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-24 21:15:01 +01:00
41e7cec72f Re-export bytes::Buf and bytes::BufMut as well (#1750)
Co-authored-by: Daniel Egger <daniel.egger@axiros.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-24 20:31:23 +01:00
d45a1aa6b6 Add web::ReqData<T> extractor (#1748)
Co-authored-by: Jonas Platte <jonas@lumeo.com>
2020-10-24 18:49:50 +01:00
98243db9f1 Print unconfigured Data<T> type when attempting extraction (#1743)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-20 17:35:34 +01:00
f92742bdac Bump base64 to 0.13 (#1744) 2020-10-19 18:24:22 +01:00
e563025b16 always construct shortslice using debug checked new constructor (#1741) 2020-10-19 12:51:30 +01:00
cfd5b381f1 Implement Logger middleware regex exclude pattern (#1723)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-19 07:18:16 +01:00
2f84914146 Skip some tests that cause ICE on nightly (#1740) 2020-10-19 11:52:05 +09:00
d765e9099d Fix clippy::rc_buffer (#1728) 2020-10-10 09:26:05 +09:00
34b23f31c9 prepare files release 0.4.0 2020-10-06 22:08:33 +01:00
26c1a901d9 add files preference for utf8 text responses (#1714) 2020-10-06 21:56:28 +01:00
c2c71cc626 Fix/suppress clippy warnings (#1720) 2020-10-01 18:19:09 +09:00
52 changed files with 1213 additions and 291 deletions

View File

@ -1,6 +1,24 @@
# Changes
## Unreleased - 2020-xx-xx
### Added
* Implement `exclude_regex` for Logger middleware. [#1723]
* Add request-local data extractor `web::ReqData`. [#1748]
* Add ability to register closure for request middleware logging. [#1749]
* Add `app_data` to `ServiceConfig`. [#1757]
* Expose `on_connect` for access to the connection stream before request is handled. [#1754]
### Changed
* Print non-configured `Data<T>` type when attempting extraction. [#1743]
* Re-export bytes::Buf{Mut} in web module. [#1750]
* Upgrade `pin-project` to `1.0`.
[#1723]: https://github.com/actix/actix-web/pull/1723
[#1743]: https://github.com/actix/actix-web/pull/1743
[#1748]: https://github.com/actix/actix-web/pull/1748
[#1750]: https://github.com/actix/actix-web/pull/1750
[#1754]: https://github.com/actix/actix-web/pull/1754
[#1749]: https://github.com/actix/actix-web/pull/1749
## 3.1.0 - 2020-09-29

View File

@ -64,6 +64,14 @@ required-features = ["compress"]
name = "test_server"
required-features = ["compress"]
[[example]]
name = "on_connect"
required-features = []
[[example]]
name = "client"
required-features = ["rustls"]
[dependencies]
actix-codec = "0.3.0"
actix-service = "1.0.6"
@ -90,8 +98,8 @@ fxhash = "0.2.1"
log = "0.4"
mime = "0.3"
socket2 = "0.3"
pin-project = "0.4.17"
regex = "1.3"
pin-project = "1.0.0"
regex = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.6.1"
@ -105,7 +113,7 @@ tinyvec = { version = "1", features = ["alloc"] }
actix = "0.10.0"
actix-http = { version = "2.0.0", features = ["actors"] }
rand = "0.7"
env_logger = "0.7"
env_logger = "0.8"
serde_derive = "1.0"
brotli2 = "0.3.2"
flate2 = "1.0.13"
@ -125,10 +133,6 @@ actix-files = { path = "actix-files" }
actix-multipart = { path = "actix-multipart" }
awc = { path = "awc" }
[[example]]
name = "client"
required-features = ["rustls"]
[[bench]]
name = "server"
harness = false

View File

@ -1,12 +1,24 @@
# Changes
## [Unreleased] - 2020-xx-xx
## Unreleased - 2020-xx-xx
## [0.3.0-beta.1] - 2020-07-15
## 0.4.0 - 2020-10-06
* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
[#1714]: https://github.com/actix/actix-web/pull/1714
## 0.3.0 - 2020-09-11
* No significant changes from 0.3.0-beta.1.
## 0.3.0-beta.1 - 2020-07-15
* Update `v_htmlescape` to 0.10
* Update `actix-web` and `actix-http` dependencies to beta.1
## [0.3.0-alpha.1] - 2020-05-23
## 0.3.0-alpha.1 - 2020-05-23
* Update `actix-web` and `actix-http` dependencies to alpha
* Fix some typos in the docs
* Bump minimum supported Rust version to 1.40
@ -14,77 +26,73 @@
[#1384]: https://github.com/actix/actix-web/pull/1384
## [0.2.1] - 2019-12-22
## 0.2.1 - 2019-12-22
* Use the same format for file URLs regardless of platforms
## [0.2.0] - 2019-12-20
## 0.2.0 - 2019-12-20
* Fix BodyEncoding trait import #1220
## [0.2.0-alpha.1] - 2019-12-07
## 0.2.0-alpha.1 - 2019-12-07
* Migrate to `std::future`
## [0.1.7] - 2019-11-06
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
## [0.1.6] - 2019-10-14
## 0.1.7 - 2019-11-06
* Add an additional `filename*` param in the `Content-Disposition` header of
`actix_files::NamedFile` to be more compatible. (#1151)
## 0.1.6 - 2019-10-14
* Add option to redirect to a slash-ended path `Files` #1132
## [0.1.5] - 2019-10-08
## 0.1.5 - 2019-10-08
* Bump up `mime_guess` crate version to 2.0.1
* Bump up `percent-encoding` crate version to 2.1
* Allow user defined request guards for `Files` #1113
## [0.1.4] - 2019-07-20
## 0.1.4 - 2019-07-20
* Allow to disable `Content-Disposition` header #686
## [0.1.3] - 2019-06-28
## 0.1.3 - 2019-06-28
* Do not set `Content-Length` header, let actix-http set it #930
## [0.1.2] - 2019-06-13
## 0.1.2 - 2019-06-13
* Content-Length is 0 for NamedFile HEAD request #914
* Fix ring dependency from actix-web default features for #741
## [0.1.1] - 2019-06-01
## 0.1.1 - 2019-06-01
* Static files are incorrectly served as both chunked and with length #812
## [0.1.0] - 2019-05-25
* NamedFile last-modified check always fails due to nano-seconds
in file modified date #820
## 0.1.0 - 2019-05-25
* NamedFile last-modified check always fails due to nano-seconds in file modified date #820
## [0.1.0-beta.4] - 2019-05-12
## 0.1.0-beta.4 - 2019-05-12
* Update actix-web to beta.4
## [0.1.0-beta.1] - 2019-04-20
## 0.1.0-beta.1 - 2019-04-20
* Update actix-web to beta.1
## [0.1.0-alpha.6] - 2019-04-14
## 0.1.0-alpha.6 - 2019-04-14
* Update actix-web to alpha6
## [0.1.0-alpha.4] - 2019-04-08
## 0.1.0-alpha.4 - 2019-04-08
* Update actix-web to alpha4
## [0.1.0-alpha.2] - 2019-04-02
## 0.1.0-alpha.2 - 2019-04-02
* Add default handler support
## [0.1.0-alpha.1] - 2019-03-28
## 0.1.0-alpha.1 - 2019-03-28
* Initial impl

View File

@ -1,8 +1,8 @@
[package]
name = "actix-files"
version = "0.3.0"
version = "0.4.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static files support for actix web."
description = "Static file serving for Actix Web"
readme = "README.md"
keywords = ["actix", "http", "async", "futures"]
homepage = "https://actix.rs"

View File

@ -1,9 +1,19 @@
# Static files support for actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-files)](https://crates.io/crates/actix-files) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
# actix-files
## Documentation & community resources
> Static file serving for Actix Web
* [User Guide](https://actix.rs/docs/)
* [API Documentation](https://docs.rs/actix-files/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-files](https://crates.io/crates/actix-files)
* Minimum supported Rust version: 1.40 or later
[![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg)](https://docs.rs/actix-files)
[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
![License](https://img.shields.io/crates/l/actix-files.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-files/0.4.0/status.svg)](https://deps.rs/crate/actix-files/0.4.0)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-files/)
- [Example Project](https://github.com/actix/examples/tree/master/static_index)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum supported Rust version: 1.42 or later

View File

@ -0,0 +1,52 @@
use mime::Mime;
/// Transforms MIME `text/*` types into their UTF-8 equivalent, if supported.
///
/// MIME types that are converted
/// - application/javascript
/// - text/html
/// - text/css
/// - text/plain
/// - text/csv
/// - text/tab-separated-values
pub(crate) fn equiv_utf8_text(ct: Mime) -> Mime {
// use (roughly) order of file-type popularity for a web server
if ct == mime::APPLICATION_JAVASCRIPT {
return mime::APPLICATION_JAVASCRIPT_UTF_8;
}
if ct == mime::TEXT_HTML {
return mime::TEXT_HTML_UTF_8;
}
if ct == mime::TEXT_CSS {
return mime::TEXT_CSS_UTF_8;
}
if ct == mime::TEXT_PLAIN {
return mime::TEXT_PLAIN_UTF_8;
}
if ct == mime::TEXT_CSV {
return mime::TEXT_CSV_UTF_8;
}
if ct == mime::TEXT_TAB_SEPARATED_VALUES {
return mime::TEXT_TAB_SEPARATED_VALUES_UTF_8;
}
ct
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_equiv_utf8_text() {
assert_eq!(equiv_utf8_text(mime::TEXT_PLAIN), mime::TEXT_PLAIN_UTF_8);
assert_eq!(equiv_utf8_text(mime::TEXT_XML), mime::TEXT_XML);
assert_eq!(equiv_utf8_text(mime::IMAGE_PNG), mime::IMAGE_PNG);
}
}

View File

@ -138,24 +138,33 @@ impl Files {
self
}
#[inline]
/// Specifies whether to use ETag or not.
///
/// Default is true.
#[inline]
pub fn use_etag(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::ETAG, value);
self
}
#[inline]
/// Specifies whether to use Last-Modified or not.
///
/// Default is true.
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::LAST_MD, value);
self
}
/// Specifies whether text responses should signal a UTF-8 encoding.
///
/// Default is false (but will default to true in a future version).
#[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::PREFER_UTF8, value);
self
}
/// Specifies custom guards to use for directory listings and files.
///
/// Default behaviour allows GET and HEAD.

View File

@ -1,4 +1,4 @@
//! Static files support for Actix Web.
//! Static file serving for Actix Web.
//!
//! Provides a non-blocking service for serving static files from disk.
//!
@ -8,12 +8,8 @@
//! use actix_files::Files;
//!
//! let app = App::new()
//! .service(Files::new("/static", "."));
//! .service(Files::new("/static", ".").prefer_utf8(true));
//! ```
//!
//! # Implementation Quirks
//! - If a filename contains non-ascii characters, that file will be served with the `charset=utf-8`
//! extension on the Content-Type header.
#![deny(rust_2018_idioms)]
#![warn(missing_docs, missing_debug_implementations)]
@ -30,6 +26,7 @@ use mime_guess::from_ext;
mod chunked;
mod directory;
mod encoding;
mod error;
mod files;
mod named;
@ -93,6 +90,9 @@ mod tests {
#[actix_rt::test]
async fn test_file_extension_to_mime() {
let m = file_extension_to_mime("");
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
let m = file_extension_to_mime("jpg");
assert_eq!(m, mime::IMAGE_JPEG);

View File

@ -22,20 +22,21 @@ use bitflags::bitflags;
use futures_util::future::{ready, Ready};
use mime_guess::from_path;
use crate::range::HttpRange;
use crate::ChunkedReadFile;
use crate::{encoding::equiv_utf8_text, range::HttpRange};
bitflags! {
pub(crate) struct Flags: u8 {
const ETAG = 0b0000_0001;
const LAST_MD = 0b0000_0010;
const ETAG = 0b0000_0001;
const LAST_MD = 0b0000_0010;
const CONTENT_DISPOSITION = 0b0000_0100;
const PREFER_UTF8 = 0b0000_1000;
}
}
impl Default for Flags {
fn default() -> Self {
Flags::all()
Flags::from_bits_truncate(0b0000_0111)
}
}
@ -92,6 +93,7 @@ impl NamedFile {
};
let ct = from_path(&path).first_or_octet_stream();
let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment,
@ -215,24 +217,33 @@ impl NamedFile {
self
}
#[inline]
///Specifies whether to use ETag or not.
/// Specifies whether to use ETag or not.
///
///Default is true.
/// Default is true.
#[inline]
pub fn use_etag(mut self, value: bool) -> Self {
self.flags.set(Flags::ETAG, value);
self
}
#[inline]
///Specifies whether to use Last-Modified or not.
/// Specifies whether to use Last-Modified or not.
///
///Default is true.
/// Default is true.
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self {
self.flags.set(Flags::LAST_MD, value);
self
}
/// Specifies whether text responses should signal a UTF-8 encoding.
///
/// Default is false (but will default to true in a future version).
#[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self {
self.flags.set(Flags::PREFER_UTF8, value);
self
}
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
// This etag format is similar to Apache's.
self.modified.as_ref().map(|mtime| {
@ -268,18 +279,24 @@ impl NamedFile {
/// Creates an `HttpResponse` with file as a streaming body.
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
if self.status_code != StatusCode::OK {
let mut resp = HttpResponse::build(self.status_code);
let mut res = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone()))
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
res.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
});
if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone());
res.header(header::CONTENT_TYPE, ct.to_string());
} else {
res.header(header::CONTENT_TYPE, self.content_type.to_string());
}
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
}
if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding);
res.encoding(current_encoding);
}
let reader = ChunkedReadFile {
@ -290,7 +307,7 @@ impl NamedFile {
counter: 0,
};
return Ok(resp.streaming(reader));
return Ok(res.streaming(reader));
}
let etag = if self.flags.contains(Flags::ETAG) {
@ -342,25 +359,33 @@ impl NamedFile {
};
let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone()))
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
res.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
});
if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone());
resp.header(header::CONTENT_TYPE, ct.to_string());
} else {
resp.header(header::CONTENT_TYPE, self.content_type.to_string());
}
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
resp.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
}
// default compressing
if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding);
}
resp.if_some(last_modified, |lm, resp| {
resp.set(header::LastModified(lm));
})
.if_some(etag, |etag, resp| {
resp.set(header::ETag(etag));
});
if let Some(lm) = last_modified {
resp.header(header::LAST_MODIFIED, lm.to_string());
}
if let Some(etag) = etag {
resp.header(header::ETAG, etag.to_string());
}
resp.header(header::ACCEPT_RANGES, "bytes");

View File

@ -0,0 +1,40 @@
use actix_files::Files;
use actix_web::{
http::{
header::{self, HeaderValue},
StatusCode,
},
test::{self, TestRequest},
App,
};
#[actix_rt::test]
async fn test_utf8_file_contents() {
// use default ISO-8859-1 encoding
let mut srv =
test::init_service(App::new().service(Files::new("/", "./tests"))).await;
let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain")),
);
// prefer UTF-8 encoding
let mut srv = test::init_service(
App::new().service(Files::new("/", "./tests").prefer_utf8(true)),
)
.await;
let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
);
}

View File

@ -0,0 +1,3 @@
中文内容显示正确。
English is OK.

View File

@ -3,6 +3,21 @@
## Unreleased - 2020-xx-xx
## 2.1.0 - 2020-10-30
### Added
* Added more flexible `on_connect_ext` methods for on-connect handling. [#1754]
### Changed
* Upgrade `base64` to `0.13`. [#1744]
* Upgrade `pin-project` to `1.0`. [#1733]
* Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760]
[#1760]: https://github.com/actix/actix-web/pull/1760
[#1754]: https://github.com/actix/actix-web/pull/1754
[#1733]: https://github.com/actix/actix-web/pull/1733
[#1744]: https://github.com/actix/actix-web/pull/1744
## 2.0.0 - 2020-09-11
* No significant changes from `2.0.0-beta.4`.

View File

@ -1,8 +1,8 @@
[package]
name = "actix-http"
version = "2.0.0"
version = "2.1.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix HTTP primitives"
description = "HTTP primitives for the Actix ecosystem"
readme = "README.md"
keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs"
@ -49,7 +49,7 @@ actix-threadpool = "0.3.1"
actix-tls = { version = "2.0.0", optional = true }
actix = { version = "0.10.0", optional = true }
base64 = "0.12"
base64 = "0.13"
bitflags = "1.2"
bytes = "0.5.3"
cookie = { version = "0.14.1", features = ["percent-encode"] }
@ -71,7 +71,7 @@ language-tags = "0.2"
log = "0.4"
mime = "0.3"
percent-encoding = "2.1"
pin-project = "0.4.17"
pin-project = "1.0.0"
rand = "0.7"
regex = "1.3"
serde = "1.0"

View File

@ -1,14 +1,18 @@
# Actix http [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-http)](https://crates.io/crates/actix-http) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
# actix-http
Actix http
> HTTP primitives for the Actix ecosystem.
## Documentation & community resources
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=2.1.0)](https://docs.rs/actix-http/2.1.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http)
[![Dependency Status](https://deps.rs/crate/actix-http/2.1.0/status.svg)](https://deps.rs/crate/actix-http/2.1.0)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
* [User Guide](https://actix.rs/docs/)
* [API Documentation](https://docs.rs/actix-http/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-http](https://crates.io/crates/actix-http)
* Minimum supported Rust version: 1.40 or later
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http/2.1.0)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.42.0
## Example

View File

@ -14,10 +14,11 @@ use crate::helpers::{Data, DataFactory};
use crate::request::Request;
use crate::response::Response;
use crate::service::HttpService;
use crate::{ConnectCallback, Extensions};
/// A http service builder
/// A HTTP service builder
///
/// This type can be used to construct an instance of `http service` through a
/// This type can be used to construct an instance of [`HttpService`] through a
/// builder-like pattern.
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
keep_alive: KeepAlive,
@ -27,7 +28,9 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
local_addr: Option<net::SocketAddr>,
expect: X,
upgrade: Option<U>,
// DEPRECATED: in favor of on_connect_ext
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, S)>,
}
@ -49,6 +52,7 @@ where
expect: ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_t: PhantomData,
}
}
@ -138,6 +142,7 @@ where
expect: expect.into_factory(),
upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
}
}
@ -167,14 +172,16 @@ where
expect: self.expect,
upgrade: Some(upgrade.into_factory()),
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
}
}
/// Set on-connect callback.
///
/// It get called once per connection and result of the call
/// get stored to the request's extensions.
/// Called once per connection. Return value of the call is stored in request extensions.
///
/// *SOFT DEPRECATED*: Prefer the `on_connect_ext` style callback.
pub fn on_connect<F, I>(mut self, f: F) -> Self
where
F: Fn(&T) -> I + 'static,
@ -184,7 +191,20 @@ where
self
}
/// Finish service configuration and create *http service* for HTTP/1 protocol.
/// Sets the callback to be run on connection establishment.
///
/// Has mutable access to a data container that will be merged into request extensions.
/// This enables transport layer data (like client certificates) to be accessed in middleware
/// and handlers.
pub fn on_connect_ext<F>(mut self, f: F) -> Self
where
F: Fn(&T, &mut Extensions) + 'static,
{
self.on_connect_ext = Some(Rc::new(f));
self
}
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol.
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
where
B: MessageBody,
@ -200,13 +220,15 @@ where
self.secure,
self.local_addr,
);
H1Service::with_config(cfg, service.into_factory())
.expect(self.expect)
.upgrade(self.upgrade)
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
}
/// Finish service configuration and create *http service* for HTTP/2 protocol.
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where
B: MessageBody + 'static,
@ -223,7 +245,10 @@ where
self.secure,
self.local_addr,
);
H2Service::with_config(cfg, service.into_factory()).on_connect(self.on_connect)
H2Service::with_config(cfg, service.into_factory())
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
}
/// Finish service configuration and create `HttpService` instance.
@ -243,9 +268,11 @@ where
self.secure,
self.local_addr,
);
HttpService::with_config(cfg, service.into_factory())
.expect(self.expect)
.upgrade(self.upgrade)
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
}
}

View File

@ -1,5 +1,5 @@
use std::any::{Any, TypeId};
use std::fmt;
use std::{fmt, mem};
use fxhash::FxHashMap;
@ -61,6 +61,16 @@ impl Extensions {
pub fn clear(&mut self) {
self.map.clear();
}
/// Extends self with the items from another `Extensions`.
pub fn extend(&mut self, other: Extensions) {
self.map.extend(other.map);
}
/// Sets (or overrides) items from `other` into this map.
pub(crate) fn drain_from(&mut self, other: &mut Self) {
self.map.extend(mem::take(&mut other.map));
}
}
impl fmt::Debug for Extensions {
@ -178,4 +188,57 @@ mod tests {
assert_eq!(extensions.get::<bool>(), None);
assert_eq!(extensions.get(), Some(&MyType(10)));
}
#[test]
fn test_extend() {
#[derive(Debug, PartialEq)]
struct MyType(i32);
let mut extensions = Extensions::new();
extensions.insert(5i32);
extensions.insert(MyType(10));
let mut other = Extensions::new();
other.insert(15i32);
other.insert(20u8);
extensions.extend(other);
assert_eq!(extensions.get(), Some(&15i32));
assert_eq!(extensions.get_mut(), Some(&mut 15i32));
assert_eq!(extensions.remove::<i32>(), Some(15i32));
assert!(extensions.get::<i32>().is_none());
assert_eq!(extensions.get::<bool>(), None);
assert_eq!(extensions.get(), Some(&MyType(10)));
assert_eq!(extensions.get(), Some(&20u8));
assert_eq!(extensions.get_mut(), Some(&mut 20u8));
}
#[test]
fn test_drain_from() {
let mut ext = Extensions::new();
ext.insert(2isize);
let mut more_ext = Extensions::new();
more_ext.insert(5isize);
more_ext.insert(5usize);
assert_eq!(ext.get::<isize>(), Some(&2isize));
assert_eq!(ext.get::<usize>(), None);
assert_eq!(more_ext.get::<isize>(), Some(&5isize));
assert_eq!(more_ext.get::<usize>(), Some(&5usize));
ext.drain_from(&mut more_ext);
assert_eq!(ext.get::<isize>(), Some(&5isize));
assert_eq!(ext.get::<usize>(), Some(&5usize));
assert_eq!(more_ext.get::<isize>(), None);
assert_eq!(more_ext.get::<usize>(), None);
}
}

View File

@ -12,7 +12,6 @@ use bytes::{Buf, BytesMut};
use log::{error, trace};
use pin_project::pin_project;
use crate::body::{Body, BodySize, MessageBody, ResponseBody};
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error};
@ -21,6 +20,10 @@ use crate::helpers::DataFactory;
use crate::httpmessage::HttpMessage;
use crate::request::Request;
use crate::response::Response;
use crate::{
body::{Body, BodySize, MessageBody, ResponseBody},
Extensions,
};
use super::codec::Codec;
use super::payload::{Payload, PayloadSender, PayloadStatus};
@ -88,6 +91,7 @@ where
expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>,
on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
flags: Flags,
peer_addr: Option<net::SocketAddr>,
error: Option<DispatchError>,
@ -167,7 +171,7 @@ where
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
{
/// Create http/1 dispatcher.
/// Create HTTP/1 dispatcher.
pub(crate) fn new(
stream: T,
config: ServiceConfig,
@ -175,6 +179,7 @@ where
expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>,
on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
peer_addr: Option<net::SocketAddr>,
) -> Self {
Dispatcher::with_timeout(
@ -187,6 +192,7 @@ where
expect,
upgrade,
on_connect,
on_connect_data,
peer_addr,
)
}
@ -202,6 +208,7 @@ where
expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>,
on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
peer_addr: Option<net::SocketAddr>,
) -> Self {
let keepalive = config.keep_alive_enabled();
@ -234,6 +241,7 @@ where
expect,
upgrade,
on_connect,
on_connect_data,
flags,
peer_addr,
ka_expire,
@ -526,11 +534,15 @@ where
let pl = this.codec.message_type();
req.head_mut().peer_addr = *this.peer_addr;
// DEPRECATED
// set on_connect data
if let Some(ref on_connect) = this.on_connect {
on_connect.set(&mut req.extensions_mut());
}
// merge on_connect_ext data into request extensions
req.extensions_mut().drain_from(this.on_connect_data);
if pl == MessageType::Stream && this.upgrade.is_some() {
this.messages.push_back(DispatcherMessage::Upgrade(req));
break;
@ -927,8 +939,10 @@ mod tests {
CloneableService::new(ExpectHandler),
None,
None,
Extensions::new(),
None,
);
match Pin::new(&mut h1).poll(cx) {
Poll::Pending => panic!(),
Poll::Ready(res) => assert!(res.is_err()),

View File

@ -18,6 +18,7 @@ use crate::error::{DispatchError, Error, ParseError};
use crate::helpers::DataFactory;
use crate::request::Request;
use crate::response::Response;
use crate::{ConnectCallback, Extensions};
use super::codec::Codec;
use super::dispatcher::Dispatcher;
@ -30,6 +31,7 @@ pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler<T>> {
expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>,
}
@ -52,6 +54,7 @@ where
expect: ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_t: PhantomData,
}
}
@ -213,6 +216,7 @@ where
srv: self.srv,
upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
}
}
@ -229,6 +233,7 @@ where
srv: self.srv,
expect: self.expect,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
}
}
@ -241,6 +246,12 @@ where
self.on_connect = f;
self
}
/// Set on connect callback.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
self
}
}
impl<T, S, B, X, U> ServiceFactory for H1Service<T, S, B, X, U>
@ -274,6 +285,7 @@ where
expect: None,
upgrade: None,
on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
cfg: Some(self.cfg.clone()),
_t: PhantomData,
}
@ -303,6 +315,7 @@ where
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: Option<ServiceConfig>,
_t: PhantomData<(T, B)>,
}
@ -352,23 +365,26 @@ where
Poll::Ready(result.map(|service| {
let this = self.as_mut().project();
H1ServiceHandler::new(
this.cfg.take().unwrap(),
service,
this.expect.take().unwrap(),
this.upgrade.take(),
this.on_connect.clone(),
this.on_connect_ext.clone(),
)
}))
}
}
/// `Service` implementation for HTTP1 transport
/// `Service` implementation for HTTP/1 transport
pub struct H1ServiceHandler<T, S: Service, B, X: Service, U: Service> {
srv: CloneableService<S>,
expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_t: PhantomData<(T, B)>,
}
@ -390,6 +406,7 @@ where
expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> H1ServiceHandler<T, S, B, X, U> {
H1ServiceHandler {
srv: CloneableService::new(srv),
@ -397,6 +414,7 @@ where
upgrade: upgrade.map(CloneableService::new),
cfg,
on_connect,
on_connect_ext,
_t: PhantomData,
}
}
@ -462,11 +480,13 @@ where
}
fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let on_connect = if let Some(ref on_connect) = self.on_connect {
Some(on_connect(&io))
} else {
None
};
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
let mut connect_extensions = Extensions::new();
if let Some(ref handler) = self.on_connect_ext {
// run on_connect_ext callback, populating connect extensions
handler(&io, &mut connect_extensions);
}
Dispatcher::new(
io,
@ -474,7 +494,8 @@ where
self.srv.clone(),
self.expect.clone(),
self.upgrade.clone(),
on_connect,
deprecated_on_connect,
connect_extensions,
addr,
)
}

View File

@ -24,6 +24,7 @@ use crate::message::ResponseHead;
use crate::payload::Payload;
use crate::request::Request;
use crate::response::Response;
use crate::Extensions;
const CHUNK_SIZE: usize = 16_384;
@ -36,6 +37,7 @@ where
service: CloneableService<S>,
connection: Connection<T, Bytes>,
on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
config: ServiceConfig,
peer_addr: Option<net::SocketAddr>,
ka_expire: Instant,
@ -56,6 +58,7 @@ where
service: CloneableService<S>,
connection: Connection<T, Bytes>,
on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
config: ServiceConfig,
timeout: Option<Delay>,
peer_addr: Option<net::SocketAddr>,
@ -82,6 +85,7 @@ where
peer_addr,
connection,
on_connect,
on_connect_data,
ka_expire,
ka_timer,
_t: PhantomData,
@ -130,11 +134,15 @@ where
head.headers = parts.headers.into();
head.peer_addr = this.peer_addr;
// DEPRECATED
// set on_connect data
if let Some(ref on_connect) = this.on_connect {
on_connect.set(&mut req.extensions_mut());
}
// merge on_connect_ext data into request extensions
req.extensions_mut().drain_from(&mut this.on_connect_data);
actix_rt::spawn(ServiceResponse::<
S::Future,
S::Response,

View File

@ -2,7 +2,7 @@ use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{net, rc};
use std::{net, rc::Rc};
use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::net::TcpStream;
@ -23,6 +23,7 @@ use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::request::Request;
use crate::response::Response;
use crate::{ConnectCallback, Extensions};
use super::dispatcher::Dispatcher;
@ -30,7 +31,8 @@ use super::dispatcher::Dispatcher;
pub struct H2Service<T, S, B> {
srv: S,
cfg: ServiceConfig,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>,
}
@ -50,19 +52,27 @@ where
H2Service {
cfg,
on_connect: None,
on_connect_ext: None,
srv: service.into_factory(),
_t: PhantomData,
}
}
/// Set on connect callback.
pub(crate) fn on_connect(
mut self,
f: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self {
self.on_connect = f;
self
}
/// Set on connect callback.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
self
}
}
impl<S, B> H2Service<TcpStream, S, B>
@ -203,6 +213,7 @@ where
fut: self.srv.new_service(()),
cfg: Some(self.cfg.clone()),
on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
_t: PhantomData,
}
}
@ -214,7 +225,8 @@ pub struct H2ServiceResponse<T, S: ServiceFactory, B> {
#[pin]
fut: S::Future,
cfg: Option<ServiceConfig>,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>,
}
@ -237,6 +249,7 @@ where
H2ServiceHandler::new(
this.cfg.take().unwrap(),
this.on_connect.clone(),
this.on_connect_ext.clone(),
service,
)
}))
@ -247,7 +260,8 @@ where
pub struct H2ServiceHandler<T, S: Service, B> {
srv: CloneableService<S>,
cfg: ServiceConfig,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>,
}
@ -261,12 +275,14 @@ where
{
fn new(
cfg: ServiceConfig,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
srv: S,
) -> H2ServiceHandler<T, S, B> {
H2ServiceHandler {
cfg,
on_connect,
on_connect_ext,
srv: CloneableService::new(srv),
_t: PhantomData,
}
@ -296,18 +312,21 @@ where
}
fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let on_connect = if let Some(ref on_connect) = self.on_connect {
Some(on_connect(&io))
} else {
None
};
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
let mut connect_extensions = Extensions::new();
if let Some(ref handler) = self.on_connect_ext {
// run on_connect_ext callback, populating connect extensions
handler(&io, &mut connect_extensions);
}
H2ServiceHandlerResponse {
state: State::Handshake(
Some(self.srv.clone()),
Some(self.cfg.clone()),
addr,
on_connect,
deprecated_on_connect,
Some(connect_extensions),
server::handshake(io),
),
}
@ -325,6 +344,7 @@ where
Option<ServiceConfig>,
Option<net::SocketAddr>,
Option<Box<dyn DataFactory>>,
Option<Extensions>,
Handshake<T, Bytes>,
),
}
@ -360,6 +380,7 @@ where
ref mut config,
ref peer_addr,
ref mut on_connect,
ref mut on_connect_data,
ref mut handshake,
) => match Pin::new(handshake).poll(cx) {
Poll::Ready(Ok(conn)) => {
@ -367,6 +388,7 @@ where
srv.take().unwrap(),
conn,
on_connect.take(),
on_connect_data.take().unwrap(),
config.take().unwrap(),
None,
*peer_addr,

View File

@ -50,6 +50,7 @@ impl<'a> io::Write for Writer<'a> {
self.0.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}

View File

@ -1,4 +1,4 @@
//! Basic http primitives for actix-net framework.
//! HTTP primitives for the Actix ecosystem.
#![deny(rust_2018_idioms)]
#![allow(
@ -7,6 +7,9 @@
clippy::new_without_default,
clippy::borrow_interior_mutable_const
)]
#![allow(clippy::manual_strip)] // Allow this to keep MSRV(1.42).
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#[macro_use]
extern crate log;
@ -77,3 +80,5 @@ pub enum Protocol {
Http1,
Http2,
}
type ConnectCallback<IO> = dyn Fn(&IO, &mut Extensions);

View File

@ -554,8 +554,9 @@ impl ResponseBuilder {
self
}
/// This method calls provided closure with builder reference if value is
/// true.
/// This method calls provided closure with builder reference if value is `true`.
#[doc(hidden)]
#[deprecated = "Use an if statement."]
pub fn if_true<F>(&mut self, value: bool, f: F) -> &mut Self
where
F: FnOnce(&mut ResponseBuilder),
@ -566,8 +567,9 @@ impl ResponseBuilder {
self
}
/// This method calls provided closure with builder reference if value is
/// Some.
/// This method calls provided closure with builder reference if value is `Some`.
#[doc(hidden)]
#[deprecated = "Use an if-let construction."]
pub fn if_some<T, F>(&mut self, value: Option<T>, f: F) -> &mut Self
where
F: FnOnce(T, &mut ResponseBuilder),

View File

@ -1,7 +1,7 @@
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, net, rc};
use std::{fmt, net, rc::Rc};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream;
@ -20,15 +20,17 @@ use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::request::Request;
use crate::response::Response;
use crate::{h1, h2::Dispatcher, Protocol};
use crate::{h1, h2::Dispatcher, ConnectCallback, Extensions, Protocol};
/// `ServiceFactory` HTTP1.1/HTTP2 transport implementation
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler<T>> {
srv: S,
cfg: ServiceConfig,
expect: X,
upgrade: Option<U>,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
// DEPRECATED: in favor of on_connect_ext
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>,
}
@ -66,6 +68,7 @@ where
expect: h1::ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_t: PhantomData,
}
}
@ -81,6 +84,7 @@ where
expect: h1::ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_t: PhantomData,
}
}
@ -113,6 +117,7 @@ where
srv: self.srv,
upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
}
}
@ -138,6 +143,7 @@ where
srv: self.srv,
expect: self.expect,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
}
}
@ -145,11 +151,17 @@ where
/// Set on connect callback.
pub(crate) fn on_connect(
mut self,
f: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self {
self.on_connect = f;
self
}
/// Set connect callback with mutable access to request data container.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
self
}
}
impl<S, B, X, U> HttpService<TcpStream, S, B, X, U>
@ -355,6 +367,7 @@ where
expect: None,
upgrade: None,
on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
cfg: self.cfg.clone(),
_t: PhantomData,
}
@ -378,7 +391,8 @@ pub struct HttpServiceResponse<
fut_upg: Option<U::Future>,
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_t: PhantomData<(T, B)>,
}
@ -429,6 +443,7 @@ where
.fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)));
Poll::Ready(result.map(|service| {
let this = self.as_mut().project();
HttpServiceHandler::new(
@ -437,6 +452,7 @@ where
this.expect.take().unwrap(),
this.upgrade.take(),
this.on_connect.clone(),
this.on_connect_ext.clone(),
)
}))
}
@ -448,7 +464,8 @@ pub struct HttpServiceHandler<T, S: Service, B, X: Service, U: Service> {
expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>,
cfg: ServiceConfig,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B, X)>,
}
@ -469,11 +486,13 @@ where
srv: S,
expect: X,
upgrade: Option<U>,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> HttpServiceHandler<T, S, B, X, U> {
HttpServiceHandler {
cfg,
on_connect,
on_connect_ext,
srv: CloneableService::new(srv),
expect: CloneableService::new(expect),
upgrade: upgrade.map(CloneableService::new),
@ -543,11 +562,12 @@ where
}
fn call(&mut self, (io, proto, peer_addr): Self::Request) -> Self::Future {
let on_connect = if let Some(ref on_connect) = self.on_connect {
Some(on_connect(&io))
} else {
None
};
let mut connect_extensions = Extensions::new();
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
if let Some(ref handler) = self.on_connect_ext {
handler(&io, &mut connect_extensions);
}
match proto {
Protocol::Http2 => HttpServiceHandlerResponse {
@ -555,10 +575,12 @@ where
server::handshake(io),
self.cfg.clone(),
self.srv.clone(),
on_connect,
deprecated_on_connect,
connect_extensions,
peer_addr,
))),
},
Protocol::Http1 => HttpServiceHandlerResponse {
state: State::H1(h1::Dispatcher::new(
io,
@ -566,7 +588,8 @@ where
self.srv.clone(),
self.expect.clone(),
self.upgrade.clone(),
on_connect,
deprecated_on_connect,
connect_extensions,
peer_addr,
)),
},
@ -595,6 +618,7 @@ where
ServiceConfig,
CloneableService<S>,
Option<Box<dyn DataFactory>>,
Extensions,
Option<net::SocketAddr>,
)>,
),
@ -670,9 +694,16 @@ where
} else {
panic!()
};
let (_, cfg, srv, on_connect, peer_addr) = data.take().unwrap();
let (_, cfg, srv, on_connect, on_connect_data, peer_addr) =
data.take().unwrap();
self.set(State::H2(Dispatcher::new(
srv, conn, on_connect, cfg, None, peer_addr,
srv,
conn,
on_connect,
on_connect_data,
cfg,
None,
peer_addr,
)));
self.poll(cx)
}

View File

@ -4,7 +4,9 @@ use std::ptr::copy_nonoverlapping;
use std::slice;
// Holds a slice guaranteed to be shorter than 8 bytes
struct ShortSlice<'a>(&'a mut [u8]);
struct ShortSlice<'a> {
inner: &'a mut [u8],
}
impl<'a> ShortSlice<'a> {
/// # Safety
@ -12,10 +14,11 @@ impl<'a> ShortSlice<'a> {
unsafe fn new(slice: &'a mut [u8]) -> Self {
// Sanity check for debug builds
debug_assert!(slice.len() < 8);
ShortSlice(slice)
ShortSlice { inner: slice }
}
fn len(&self) -> usize {
self.0.len()
self.inner.len()
}
}
@ -56,7 +59,7 @@ pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) {
fn xor_short(buf: ShortSlice<'_>, mask: u64) {
// SAFETY: we know that a `ShortSlice` fits in a u64
unsafe {
let (ptr, len) = (buf.0.as_mut_ptr(), buf.0.len());
let (ptr, len) = (buf.inner.as_mut_ptr(), buf.len());
let mut b: u64 = 0;
#[allow(trivial_casts)]
copy_nonoverlapping(ptr, &mut b as *mut _ as *mut u8, len);
@ -96,7 +99,13 @@ fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) {
// SAFETY: we know the middle section is correctly aligned, and the outer
// sections are smaller than 8 bytes
unsafe { (ShortSlice::new(head), cast_slice(mid), ShortSlice(tail)) }
unsafe {
(
ShortSlice::new(head),
cast_slice(mid),
ShortSlice::new(tail),
)
}
} else {
// We didn't cross even one aligned boundary!

View File

@ -411,8 +411,10 @@ async fn test_h2_on_connect() {
let srv = test_server(move || {
HttpService::build()
.on_connect(|_| 10usize)
.on_connect_ext(|_, data| data.insert(20isize))
.h2(|req: Request| {
assert!(req.extensions().contains::<usize>());
assert!(req.extensions().contains::<isize>());
ok::<_, ()>(Response::Ok().finish())
})
.openssl(ssl_acceptor())

View File

@ -663,8 +663,10 @@ async fn test_h1_on_connect() {
let srv = test_server(|| {
HttpService::build()
.on_connect(|_| 10usize)
.on_connect_ext(|_, data| data.insert(20isize))
.h1(|req: Request| {
assert!(req.extensions().contains::<usize>());
assert!(req.extensions().contains::<isize>());
future::ok::<_, ()>(Response::Ok().finish())
})
.tcp()

View File

@ -1,7 +1,7 @@
# Changes
## Unreleased - 2020-xx-xx
* Upgrade `pin-project` to `1.0`.
## 3.0.0 - 2020-09-11
* No significant changes from `3.0.0-beta.2`.

View File

@ -23,7 +23,7 @@ actix-codec = "0.3.0"
bytes = "0.5.2"
futures-channel = { version = "0.3.5", default-features = false }
futures-core = { version = "0.3.5", default-features = false }
pin-project = "0.4.17"
pin-project = "1.0.0"
[dev-dependencies]
actix-rt = "1.1.1"

View File

@ -6,9 +6,8 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/simple-fail.rs");
t.pass("tests/trybuild/route-ok.rs");
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
test_route_duplicate_unexpected_method(&t);
test_route_missing_method(&t)
}
@ -25,3 +24,13 @@ fn test_route_missing_method(t: &trybuild::TestCases) {
#[rustversion::nightly]
fn test_route_missing_method(_t: &trybuild::TestCases) {}
// FIXME: Re-test them on nightly once rust-lang/rust#77993 is fixed.
#[rustversion::not(nightly)]
fn test_route_duplicate_unexpected_method(t: &trybuild::TestCases) {
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
}
#[rustversion::nightly]
fn test_route_duplicate_unexpected_method(_t: &trybuild::TestCases) {}

View File

@ -3,6 +3,20 @@
## Unreleased - 2020-xx-xx
## 2.0.1 - 2020-xx-xx
### Changed
* Upgrade `base64` to `0.13`. [#1744]
* Deprecate `ClientRequest::{if_some, if_true}`. [#1760]
### Fixed
* Use `Accept-Encoding: identity` instead of `Accept-Encoding: br` when no compression feature
is enabled [#1737]
[#1737]: https://github.com/actix/actix-web/pull/1737
[#1760]: https://github.com/actix/actix-web/pull/1760
[#1744]: https://github.com/actix/actix-web/pull/1744
## 2.0.0 - 2020-09-11
### Changed
* `Client::build` was renamed to `Client::builder`.

View File

@ -1,8 +1,8 @@
[package]
name = "awc"
version = "2.0.0"
version = "2.0.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP client library that uses the Actix runtime."
description = "Async HTTP and WebSocket client library built on the Actix ecosystem"
readme = "README.md"
keywords = ["actix", "http", "framework", "async", "web"]
homepage = "https://actix.rs"
@ -42,8 +42,9 @@ actix-service = "1.0.6"
actix-http = "2.0.0"
actix-rt = "1.0.0"
base64 = "0.12"
base64 = "0.13"
bytes = "0.5.3"
cfg-if = "1.0"
derive_more = "0.99.2"
futures-core = { version = "0.3.5", default-features = false }
log =" 0.4"

View File

@ -1,14 +1,19 @@
# Actix http client [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/awc)](https://crates.io/crates/awc) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
# awc (Actix Web Client)
An HTTP Client
> Async HTTP and WebSocket client library.
## Documentation & community resources
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=2.0.1)](https://docs.rs/awc/2.0.1)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/2.0.1/status.svg)](https://deps.rs/crate/awc/2.0.1)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
* [User Guide](https://actix.rs/docs/)
* [API Documentation](https://docs.rs/awc/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [awc](https://crates.io/crates/awc)
* Minimum supported Rust version: 1.40 or later
## Documentation & Resources
- [API Documentation](https://docs.rs/awc/2.0.1)
- [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
## Example

View File

@ -1,11 +1,4 @@
#![deny(rust_2018_idioms)]
#![allow(
clippy::type_complexity,
clippy::borrow_interior_mutable_const,
clippy::needless_doctest_main
)]
//! `awc` is a HTTP and WebSocket client library built using the Actix ecosystem.
//! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem.
//!
//! ## Making a GET request
//!
@ -91,6 +84,15 @@
//! # }
//! ```
#![deny(rust_2018_idioms)]
#![allow(
clippy::type_complexity,
clippy::borrow_interior_mutable_const,
clippy::needless_doctest_main
)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
use std::cell::RefCell;
use std::convert::TryFrom;
use std::rc::Rc;

View File

@ -21,10 +21,15 @@ use crate::frozen::FrozenClientRequest;
use crate::sender::{PrepForSendingError, RequestSender, SendClientRequest};
use crate::ClientConfig;
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
const HTTPS_ENCODING: &str = "br, gzip, deflate";
#[cfg(not(any(feature = "flate2-zlib", feature = "flate2-rust")))]
const HTTPS_ENCODING: &str = "br";
cfg_if::cfg_if! {
if #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] {
const HTTPS_ENCODING: &str = "br, gzip, deflate";
} else if #[cfg(feature = "compress")] {
const HTTPS_ENCODING: &str = "br";
} else {
const HTTPS_ENCODING: &str = "identity";
}
}
/// An HTTP Client request builder
///
@ -349,8 +354,9 @@ impl ClientRequest {
self
}
/// This method calls provided closure with builder reference if
/// value is `true`.
/// This method calls provided closure with builder reference if value is `true`.
#[doc(hidden)]
#[deprecated = "Use an if statement."]
pub fn if_true<F>(self, value: bool, f: F) -> Self
where
F: FnOnce(ClientRequest) -> ClientRequest,
@ -362,8 +368,9 @@ impl ClientRequest {
}
}
/// This method calls provided closure with builder reference if
/// value is `Some`.
/// This method calls provided closure with builder reference if value is `Some`.
#[doc(hidden)]
#[deprecated = "Use an if-let construction."]
pub fn if_some<T, F>(self, value: Option<T>, f: F) -> Self
where
F: FnOnce(T, ClientRequest) -> ClientRequest,
@ -596,20 +603,27 @@ mod tests {
#[actix_rt::test]
async fn test_basics() {
let mut req = Client::new()
let req = Client::new()
.put("/")
.version(Version::HTTP_2)
.set(header::Date(SystemTime::now().into()))
.content_type("plain/text")
.if_true(true, |req| req.header(header::SERVER, "awc"))
.if_true(false, |req| req.header(header::EXPECT, "awc"))
.if_some(Some("server"), |val, req| {
req.header(header::USER_AGENT, val)
})
.if_some(Option::<&str>::None, |_, req| {
req.header(header::ALLOW, "1")
})
.content_length(100);
.header(header::SERVER, "awc");
let req = if let Some(val) = Some("server") {
req.header(header::USER_AGENT, val)
} else {
req
};
let req = if let Some(_val) = Option::<&str>::None {
req.header(header::ALLOW, "1")
} else {
req
};
let mut req = req.content_length(100);
assert!(req.headers().contains_key(header::CONTENT_TYPE));
assert!(req.headers().contains_key(header::DATE));
assert!(req.headers().contains_key(header::SERVER));
@ -617,6 +631,7 @@ mod tests {
assert!(!req.headers().contains_key(header::ALLOW));
assert!(!req.headers().contains_key(header::EXPECT));
assert_eq!(req.head.version, Version::HTTP_2);
let _ = req.headers_mut();
let _ = req.send_body("");
}

51
examples/on_connect.rs Normal file
View File

@ -0,0 +1,51 @@
//! This example shows how to use `actix_web::HttpServer::on_connect` to access a lower-level socket
//! properties and pass them to a handler through request-local data.
//!
//! For an example of extracting a client TLS certificate, see:
//! <https://github.com/actix/examples/tree/HEAD/rustls-client-cert>
use std::{any::Any, env, io, net::SocketAddr};
use actix_web::{dev::Extensions, rt::net::TcpStream, web, App, HttpServer};
#[derive(Debug, Clone)]
struct ConnectionInfo {
bind: SocketAddr,
peer: SocketAddr,
ttl: Option<u32>,
}
async fn route_whoami(conn_info: web::ReqData<ConnectionInfo>) -> String {
format!(
"Here is some info about your connection:\n\n{:#?}",
conn_info
)
}
fn get_conn_info(connection: &dyn Any, data: &mut Extensions) {
if let Some(sock) = connection.downcast_ref::<TcpStream>() {
data.insert(ConnectionInfo {
bind: sock.local_addr().unwrap(),
peer: sock.peer_addr().unwrap(),
ttl: sock.ttl().ok(),
});
} else {
unreachable!("connection should only be plaintext since no TLS is set up");
}
}
#[actix_web::main]
async fn main() -> io::Result<()> {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info");
}
env_logger::init();
HttpServer::new(|| App::new().default_service(web::to(route_whoami)))
.on_connect(get_conn_info)
.bind(("127.0.0.1", 8080))?
.workers(1)
.run()
.await
}

View File

@ -1 +0,0 @@
1.42.0

View File

@ -183,6 +183,7 @@ where
self.data.extend(cfg.data);
self.services.extend(cfg.services);
self.external.extend(cfg.external);
self.extensions.extend(cfg.extensions);
self
}
@ -459,8 +460,8 @@ where
{
fn into_factory(self) -> AppInit<T, B> {
AppInit {
data: Rc::new(self.data),
data_factories: Rc::new(self.data_factories),
data: self.data.into_boxed_slice().into(),
data_factories: self.data_factories.into_boxed_slice().into(),
endpoint: self.endpoint,
services: Rc::new(RefCell::new(self.services)),
external: RefCell::new(self.external),

View File

@ -39,8 +39,8 @@ where
{
pub(crate) endpoint: T,
pub(crate) extensions: RefCell<Option<Extensions>>,
pub(crate) data: Rc<Vec<Box<dyn DataFactory>>>,
pub(crate) data_factories: Rc<Vec<FnDataFactory>>,
pub(crate) data: Rc<[Box<dyn DataFactory>]>,
pub(crate) data_factories: Rc<[FnDataFactory]>,
pub(crate) services: Rc<RefCell<Vec<Box<dyn AppServiceFactory>>>>,
pub(crate) default: Option<Rc<HttpNewService>>,
pub(crate) factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>,
@ -88,15 +88,15 @@ where
// complete pipeline creation
*self.factory_ref.borrow_mut() = Some(AppRoutingFactory {
default,
services: Rc::new(
services
.into_iter()
.map(|(mut rdef, srv, guards, nested)| {
rmap.add(&mut rdef, nested);
(rdef, srv, RefCell::new(guards))
})
.collect(),
),
services: services
.into_iter()
.map(|(mut rdef, srv, guards, nested)| {
rmap.add(&mut rdef, nested);
(rdef, srv, RefCell::new(guards))
})
.collect::<Vec<_>>()
.into_boxed_slice()
.into(),
});
// external resources
@ -147,7 +147,7 @@ where
rmap: Rc<ResourceMap>,
config: AppConfig,
data: Rc<Vec<Box<dyn DataFactory>>>,
data: Rc<[Box<dyn DataFactory>]>,
extensions: Option<Extensions>,
_t: PhantomData<B>,
@ -273,7 +273,7 @@ where
}
pub struct AppRoutingFactory {
services: Rc<Vec<(ResourceDef, HttpNewService, RefCell<Option<Guards>>)>>,
services: Rc<[(ResourceDef, HttpNewService, RefCell<Option<Guards>>)]>,
default: Rc<HttpNewService>,
}

View File

@ -31,7 +31,7 @@ pub struct AppService {
Option<Guards>,
Option<Rc<ResourceMap>>,
)>,
service_data: Rc<Vec<Box<dyn DataFactory>>>,
service_data: Rc<[Box<dyn DataFactory>]>,
}
impl AppService {
@ -39,7 +39,7 @@ impl AppService {
pub(crate) fn new(
config: AppConfig,
default: Rc<HttpNewService>,
service_data: Rc<Vec<Box<dyn DataFactory>>>,
service_data: Rc<[Box<dyn DataFactory>]>,
) -> Self {
AppService {
config,
@ -178,6 +178,7 @@ pub struct ServiceConfig {
pub(crate) services: Vec<Box<dyn AppServiceFactory>>,
pub(crate) data: Vec<Box<dyn DataFactory>>,
pub(crate) external: Vec<ResourceDef>,
pub(crate) extensions: Extensions,
}
impl ServiceConfig {
@ -186,6 +187,7 @@ impl ServiceConfig {
services: Vec::new(),
data: Vec::new(),
external: Vec::new(),
extensions: Extensions::new(),
}
}
@ -198,6 +200,14 @@ impl ServiceConfig {
self
}
/// Set arbitrary data item.
///
/// This is same as `App::data()` method.
pub fn app_data<U: 'static>(&mut self, ext: U) -> &mut Self {
self.extensions.insert(ext);
self
}
/// Configure route for a specific path.
///
/// This is same as `App::route()` method.
@ -254,13 +264,16 @@ mod tests {
async fn test_data() {
let cfg = |cfg: &mut ServiceConfig| {
cfg.data(10usize);
cfg.app_data(15u8);
};
let mut srv =
init_service(App::new().configure(cfg).service(
web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()),
))
.await;
let mut srv = init_service(App::new().configure(cfg).service(
web::resource("/").to(|_: web::Data<usize>, req: HttpRequest| {
assert_eq!(*req.app_data::<u8>().unwrap(), 15u8);
HttpResponse::Ok()
}),
))
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);

View File

@ -1,3 +1,4 @@
use std::any::type_name;
use std::ops::Deref;
use std::sync::Arc;
@ -19,25 +20,20 @@ pub(crate) type FnDataFactory =
/// Application data.
///
/// Application data is an arbitrary data attached to the app.
/// Application data is available to all routes and could be added
/// during application configuration process
/// with `App::data()` method.
/// Application level data is a piece of arbitrary data attached to the app, scope, or resource.
/// Application data is available to all routes and can be added during the application
/// configuration process via `App::data()`.
///
/// Application data could be accessed by using `Data<T>`
/// extractor where `T` is data type.
/// Application data can be accessed by using `Data<T>` extractor where `T` is data type.
///
/// **Note**: http server accepts an application factory rather than
/// an application instance. Http server constructs an application
/// instance for each thread, thus application data must be constructed
/// multiple times. If you want to share data between different
/// threads, a shareable object should be used, e.g. `Send + Sync`. Application
/// data does not need to be `Send` or `Sync`. Internally `Data` type
/// uses `Arc`. if your data implements `Send` + `Sync` traits you can
/// use `web::Data::new()` and avoid double `Arc`.
/// **Note**: http server accepts an application factory rather than an application instance. HTTP
/// server constructs an application instance for each thread, thus application data must be
/// constructed multiple times. If you want to share data between different threads, a shareable
/// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send`
/// or `Sync`. Internally `Data` uses `Arc`.
///
/// If route data is not set for a handler, using `Data<T>` extractor would
/// cause *Internal Server Error* response.
/// If route data is not set for a handler, using `Data<T>` extractor would cause *Internal
/// Server Error* response.
///
/// ```rust
/// use std::sync::Mutex;
@ -47,7 +43,7 @@ pub(crate) type FnDataFactory =
/// counter: usize,
/// }
///
/// /// Use `Data<T>` extractor to access data in handler.
/// /// Use the `Data<T>` extractor to access data in a handler.
/// async fn index(data: web::Data<Mutex<MyData>>) -> impl Responder {
/// let mut data = data.lock().unwrap();
/// data.counter += 1;
@ -70,10 +66,6 @@ pub struct Data<T: ?Sized>(Arc<T>);
impl<T> Data<T> {
/// Create new `Data` instance.
///
/// Internally `Data` type uses `Arc`. if your data implements
/// `Send` + `Sync` traits you can use `web::Data::new()` and
/// avoid double `Arc`.
pub fn new(state: T) -> Data<T> {
Data(Arc::new(state))
}
@ -121,8 +113,9 @@ impl<T: ?Sized + 'static> FromRequest for Data<T> {
} else {
log::debug!(
"Failed to construct App-level Data extractor. \
Request path: {:?}",
req.path()
Request path: {:?} (type: {})",
req.path(),
type_name::<T>(),
);
err(ErrorInternalServerError(
"App data is not configured, to configure use App::data()",

View File

@ -81,6 +81,7 @@ mod handler;
mod info;
pub mod middleware;
mod request;
mod request_data;
mod resource;
mod responder;
mod rmap;

View File

@ -13,7 +13,7 @@ use actix_service::{Service, Transform};
use bytes::Bytes;
use futures_util::future::{ok, Ready};
use log::debug;
use regex::Regex;
use regex::{Regex, RegexSet};
use time::OffsetDateTime;
use crate::dev::{BodySize, MessageBody, ResponseBody};
@ -34,21 +34,19 @@ use crate::HttpResponse;
/// Default `Logger` could be created with `default` method, it uses the
/// default format:
///
/// ```ignore
/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
/// ```plain
/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
/// ```
///
/// ```rust
/// use actix_web::middleware::Logger;
/// use actix_web::App;
/// use actix_web::{middleware::Logger, App};
///
/// fn main() {
/// std::env::set_var("RUST_LOG", "actix_web=info");
/// env_logger::init();
/// std::env::set_var("RUST_LOG", "actix_web=info");
/// env_logger::init();
///
/// let app = App::new()
/// .wrap(Logger::default())
/// .wrap(Logger::new("%a %{User-Agent}i"));
/// }
/// let app = App::new()
/// .wrap(Logger::default())
/// .wrap(Logger::new("%a %{User-Agent}i"));
/// ```
///
/// ## Format
@ -80,6 +78,8 @@ use crate::HttpResponse;
///
/// `%{FOO}e` os.environ['FOO']
///
/// `%{FOO}xi` [custom request replacement](Logger::custom_request_replace) labelled "FOO"
///
/// # Security
/// **\*** It is calculated using
/// [`ConnectionInfo::realip_remote_addr()`](../dev/struct.ConnectionInfo.html#method.realip_remote_addr)
@ -92,6 +92,7 @@ pub struct Logger(Rc<Inner>);
struct Inner {
format: Format,
exclude: HashSet<String>,
exclude_regex: RegexSet,
}
impl Logger {
@ -100,6 +101,7 @@ impl Logger {
Logger(Rc::new(Inner {
format: Format::new(format),
exclude: HashSet::new(),
exclude_regex: RegexSet::empty(),
}))
}
@ -111,18 +113,69 @@ impl Logger {
.insert(path.into());
self
}
/// Ignore and do not log access info for paths that match regex
pub fn exclude_regex<T: Into<String>>(mut self, path: T) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap();
let mut patterns = inner.exclude_regex.patterns().to_vec();
patterns.push(path.into());
let regex_set = RegexSet::new(patterns).unwrap();
inner.exclude_regex = regex_set;
self
}
/// Register a function that receives a ServiceRequest and returns a String for use in the
/// log line. The label passed as the first argument should match a replacement substring in
/// the logger format like `%{label}xi`.
///
/// It is convention to print "-" to indicate no output instead of an empty string.
///
/// # Example
/// ```rust
/// # use actix_web::{http::HeaderValue, middleware::Logger};
/// # fn parse_jwt_id (_req: Option<&HeaderValue>) -> String { "jwt_uid".to_owned() }
/// Logger::new("example %{JWT_ID}xi")
/// .custom_request_replace("JWT_ID", |req| parse_jwt_id(req.headers().get("Authorization")));
/// ```
pub fn custom_request_replace(
mut self,
label: &str,
f: impl Fn(&ServiceRequest) -> String + 'static,
) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap();
let ft = inner.format.0.iter_mut().find(|ft| {
matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label)
});
if let Some(FormatText::CustomRequest(_, request_fn)) = ft {
// replace into None or previously registered fn using same label
request_fn.replace(CustomRequestFn {
inner_fn: Rc::new(f),
});
} else {
// non-printed request replacement function diagnostic
debug!(
"Attempted to register custom request logging function for nonexistent label: {}",
label
);
}
self
}
}
impl Default for Logger {
/// Create `Logger` middleware with format:
///
/// ```ignore
/// ```plain
/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
/// ```
fn default() -> Logger {
Logger(Rc::new(Inner {
format: Format::default(),
exclude: HashSet::new(),
exclude_regex: RegexSet::empty(),
}))
}
}
@ -140,6 +193,17 @@ where
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
for unit in &self.0.format.0 {
// missing request replacement function diagnostic
if let FormatText::CustomRequest(label, None) = unit {
debug!(
"No custom request replacement function was registered for label {} in\
logger format.",
label
);
}
}
ok(LoggerMiddleware {
service,
inner: self.0.clone(),
@ -168,7 +232,9 @@ where
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
if self.inner.exclude.contains(req.path()) {
if self.inner.exclude.contains(req.path())
|| self.inner.exclude_regex.is_match(req.path())
{
LoggerResponse {
fut: self.service.call(req),
format: None,
@ -296,7 +362,6 @@ impl<B: MessageBody> MessageBody for StreamLog<B> {
/// A formatting style for the `Logger`, consisting of multiple
/// `FormatText`s concatenated into one line.
#[derive(Clone)]
#[doc(hidden)]
struct Format(Vec<FormatText>);
impl Default for Format {
@ -312,7 +377,8 @@ impl Format {
/// Returns `None` if the format string syntax is incorrect.
pub fn new(s: &str) -> Format {
log::trace!("Access log format: {}", s);
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe])|[atPrUsbTD]?)").unwrap();
let fmt =
Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[atPrUsbTD]?)").unwrap();
let mut idx = 0;
let mut results = Vec::new();
@ -340,6 +406,7 @@ impl Format {
HeaderName::try_from(key.as_str()).unwrap(),
),
"e" => FormatText::EnvironHeader(key.as_str().to_owned()),
"xi" => FormatText::CustomRequest(key.as_str().to_owned(), None),
_ => unreachable!(),
})
} else {
@ -369,7 +436,9 @@ impl Format {
/// A string of text to be logged. This is either one of the data
/// fields supported by the `Logger`, or a custom `String`.
#[doc(hidden)]
#[non_exhaustive]
#[derive(Debug, Clone)]
// TODO: remove pub on next breaking change
pub enum FormatText {
Str(String),
Percent,
@ -385,6 +454,26 @@ pub enum FormatText {
RequestHeader(HeaderName),
ResponseHeader(HeaderName),
EnvironHeader(String),
CustomRequest(String, Option<CustomRequestFn>),
}
// TODO: remove pub on next breaking change
#[doc(hidden)]
#[derive(Clone)]
pub struct CustomRequestFn {
inner_fn: Rc<dyn Fn(&ServiceRequest) -> String>,
}
impl CustomRequestFn {
fn call(&self, req: &ServiceRequest) -> String {
(self.inner_fn)(req)
}
}
impl fmt::Debug for CustomRequestFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("custom_request_fn")
}
}
impl FormatText {
@ -441,7 +530,7 @@ impl FormatText {
}
fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) {
match *self {
match &*self {
FormatText::RequestLine => {
*self = if req.query_string().is_empty() {
FormatText::Str(format!(
@ -493,11 +582,20 @@ impl FormatText {
};
*self = s;
}
FormatText::CustomRequest(_, request_fn) => {
let s = match request_fn {
Some(f) => FormatText::Str(f.call(req)),
None => FormatText::Str("-".to_owned()),
};
*self = s;
}
_ => (),
}
}
}
/// Converter to get a String from something that writes to a Formatter.
pub(crate) struct FormatDisplay<'a>(
&'a dyn Fn(&mut Formatter<'_>) -> Result<(), fmt::Error>,
);
@ -515,7 +613,7 @@ mod tests {
use super::*;
use crate::http::{header, StatusCode};
use crate::test::TestRequest;
use crate::test::{self, TestRequest};
#[actix_rt::test]
async fn test_logger() {
@ -538,6 +636,28 @@ mod tests {
let _res = srv.call(req).await;
}
#[actix_rt::test]
async fn test_logger_exclude_regex() {
let srv = |req: ServiceRequest| {
ok(req.into_response(
HttpResponse::build(StatusCode::OK)
.header("X-Test", "ttt")
.finish(),
))
};
let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test")
.exclude_regex("\\w");
let mut srv = logger.new_transform(srv.into_service()).await.unwrap();
let req = TestRequest::with_header(
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
)
.to_srv_request();
let _res = srv.call(req).await.unwrap();
}
#[actix_rt::test]
async fn test_url_path() {
let mut format = Format::new("%T %U");
@ -662,4 +782,45 @@ mod tests {
println!("{}", s);
assert!(s.contains("192.0.2.60"));
}
#[actix_rt::test]
async fn test_custom_closure_log() {
let mut logger = Logger::new("test %{CUSTOM}xi")
.custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String {
String::from("custom_log")
});
let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
let label = match &unit {
FormatText::CustomRequest(label, _) => label,
ft => panic!("expected CustomRequest, found {:?}", ft),
};
assert_eq!(label, "CUSTOM");
let req = TestRequest::default().to_srv_request();
let now = OffsetDateTime::now_utc();
unit.render_request(now, &req);
let render = |fmt: &mut Formatter<'_>| unit.render(fmt, 1024, now);
let log_output = FormatDisplay(&render).to_string();
assert_eq!(log_output, "custom_log");
}
#[actix_rt::test]
async fn test_closure_logger_in_middleware() {
let captured = "custom log replacement";
let logger = Logger::new("%{CUSTOM}xi")
.custom_request_replace("CUSTOM", move |_req: &ServiceRequest| -> String {
captured.to_owned()
});
let mut srv = logger.new_transform(test::ok_service()).await.unwrap();
let req = TestRequest::default().to_srv_request();
srv.call(req).await.unwrap();
}
}

175
src/request_data.rs Normal file
View File

@ -0,0 +1,175 @@
use std::{any::type_name, ops::Deref};
use actix_http::error::{Error, ErrorInternalServerError};
use futures_util::future;
use crate::{dev::Payload, FromRequest, HttpRequest};
/// Request-local data extractor.
///
/// Request-local data is arbitrary data attached to an individual request, usually
/// by middleware. It can be set via `extensions_mut` on [`HttpRequest`][htr_ext_mut]
/// or [`ServiceRequest`][srv_ext_mut].
///
/// Unlike app data, request data is dropped when the request has finished processing. This makes it
/// useful as a kind of messaging system between middleware and request handlers. It uses the same
/// types-as-keys storage system as app data.
///
/// # Mutating Request Data
/// Note that since extractors must output owned data, only types that `impl Clone` can use this
/// extractor. A clone is taken of the required request data and can, therefore, not be directly
/// mutated in-place. To mutate request data, continue to use [`HttpRequest::extensions_mut`] or
/// re-insert the cloned data back into the extensions map. A `DerefMut` impl is intentionally not
/// provided to make this potential foot-gun more obvious.
///
/// # Example
/// ```rust,no_run
/// # use actix_web::{web, HttpResponse, HttpRequest, Responder};
///
/// #[derive(Debug, Clone, PartialEq)]
/// struct FlagFromMiddleware(String);
///
/// /// Use the `ReqData<T>` extractor to access request data in a handler.
/// async fn handler(
/// req: HttpRequest,
/// opt_flag: Option<web::ReqData<FlagFromMiddleware>>,
/// ) -> impl Responder {
/// // use an optional extractor if the middleware is
/// // not guaranteed to add this type of requests data
/// if let Some(flag) = opt_flag {
/// assert_eq!(&flag.into_inner(), req.extensions().get::<FlagFromMiddleware>().unwrap());
/// }
///
/// HttpResponse::Ok()
/// }
/// ```
///
/// [htr_ext_mut]: crate::HttpRequest::extensions_mut
/// [srv_ext_mut]: crate::dev::ServiceRequest::extensions_mut
#[derive(Debug, Clone)]
pub struct ReqData<T: Clone + 'static>(T);
impl<T: Clone + 'static> ReqData<T> {
/// Consumes the `ReqData`, returning it's wrapped data.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T: Clone + 'static> Deref for ReqData<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T: Clone + 'static> FromRequest for ReqData<T> {
type Config = ();
type Error = Error;
type Future = future::Ready<Result<Self, Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
if let Some(st) = req.extensions().get::<T>() {
future::ok(ReqData(st.clone()))
} else {
log::debug!(
"Failed to construct App-level ReqData extractor. \
Request path: {:?} (type: {})",
req.path(),
type_name::<T>(),
);
future::err(ErrorInternalServerError(
"Missing expected request extension data",
))
}
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc};
use futures_util::TryFutureExt as _;
use super::*;
use crate::{
dev::Service,
http::{Method, StatusCode},
test::{init_service, TestRequest},
web, App, HttpMessage, HttpResponse,
};
#[actix_rt::test]
async fn req_data_extractor() {
let mut srv = init_service(
App::new()
.wrap_fn(|req, srv| {
if req.method() == Method::POST {
req.extensions_mut().insert(42u32);
}
srv.call(req)
})
.service(web::resource("/test").to(
|req: HttpRequest, data: Option<ReqData<u32>>| {
if req.method() != Method::POST {
assert!(data.is_none());
}
if let Some(data) = data {
assert_eq!(*data, 42);
assert_eq!(
Some(data.into_inner()),
req.extensions().get::<u32>().copied()
);
}
HttpResponse::Ok()
},
)),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::post().uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn req_data_internal_mutability() {
let mut srv = init_service(
App::new()
.wrap_fn(|req, srv| {
let data_before = Rc::new(RefCell::new(42u32));
req.extensions_mut().insert(data_before);
srv.call(req).map_ok(|res| {
{
let ext = res.request().extensions();
let data_after = ext.get::<Rc<RefCell<u32>>>().unwrap();
assert_eq!(*data_after.borrow(), 53u32);
}
res
})
})
.default_service(web::to(|data: ReqData<Rc<RefCell<u32>>>| {
assert_eq!(*data.borrow(), 42);
*data.borrow_mut() += 11;
assert_eq!(*data.borrow(), 53);
HttpResponse::Ok()
})),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
}

View File

@ -1,3 +1,5 @@
#![allow(clippy::rc_buffer)] // inner value is mutated before being shared (`Rc::get_mut`)
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;

View File

@ -58,7 +58,6 @@ type BoxedResponse = LocalBoxFuture<'static, Result<ServiceResponse, Error>>;
/// * /{project_id}/path1 - responds to all http method
/// * /{project_id}/path2 - `GET` requests
/// * /{project_id}/path3 - `HEAD` requests
///
pub struct Scope<T = ScopeEndpoint> {
endpoint: T,
rdef: String,
@ -210,6 +209,9 @@ where
self.data = Some(data);
}
self.data
.get_or_insert_with(Extensions::new)
.extend(cfg.extensions);
self
}
@ -443,16 +445,17 @@ where
*self.factory_ref.borrow_mut() = Some(ScopeFactory {
data: self.data.take().map(Rc::new),
default: self.default.clone(),
services: Rc::new(
cfg.into_services()
.1
.into_iter()
.map(|(mut rdef, srv, guards, nested)| {
rmap.add(&mut rdef, nested);
(rdef, srv, RefCell::new(guards))
})
.collect(),
),
services: cfg
.into_services()
.1
.into_iter()
.map(|(mut rdef, srv, guards, nested)| {
rmap.add(&mut rdef, nested);
(rdef, srv, RefCell::new(guards))
})
.collect::<Vec<_>>()
.into_boxed_slice()
.into(),
});
// get guards
@ -474,7 +477,7 @@ where
pub struct ScopeFactory {
data: Option<Rc<Extensions>>,
services: Rc<Vec<(ResourceDef, HttpNewService, RefCell<Option<Guards>>)>>,
services: Rc<[(ResourceDef, HttpNewService, RefCell<Option<Guards>>)]>,
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
}

View File

@ -1,8 +1,14 @@
use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use std::{fmt, io, net};
use std::{
any::Any,
fmt, io,
marker::PhantomData,
net,
sync::{Arc, Mutex},
};
use actix_http::{body::MessageBody, Error, HttpService, KeepAlive, Request, Response};
use actix_http::{
body::MessageBody, Error, Extensions, HttpService, KeepAlive, Request, Response,
};
use actix_server::{Server, ServerBuilder};
use actix_service::{map_config, IntoServiceFactory, Service, ServiceFactory};
@ -64,6 +70,7 @@ where
backlog: i32,
sockets: Vec<Socket>,
builder: ServerBuilder,
on_connect_fn: Option<Arc<dyn Fn(&dyn Any, &mut Extensions) + Send + Sync>>,
_t: PhantomData<(S, B)>,
}
@ -91,6 +98,32 @@ where
backlog: 1024,
sockets: Vec::new(),
builder: ServerBuilder::default(),
on_connect_fn: None,
_t: PhantomData,
}
}
/// Sets function that will be called once before each connection is handled.
/// It will receive a `&std::any::Any`, which contains underlying connection type and an
/// [Extensions] container so that request-local data can be passed to middleware and handlers.
///
/// For example:
/// - `actix_tls::openssl::SslStream<actix_web::rt::net::TcpStream>` when using openssl.
/// - `actix_tls::rustls::TlsStream<actix_web::rt::net::TcpStream>` when using rustls.
/// - `actix_web::rt::net::TcpStream` when no encryption is used.
///
/// See `on_connect` example for additional details.
pub fn on_connect<CB>(self, f: CB) -> HttpServer<F, I, S, B>
where
CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static,
{
HttpServer {
factory: self.factory,
config: self.config,
backlog: self.backlog,
sockets: self.sockets,
builder: self.builder,
on_connect_fn: Some(Arc::new(f)),
_t: PhantomData,
}
}
@ -240,6 +273,7 @@ where
addr,
scheme: "http",
});
let on_connect_fn = self.on_connect_fn.clone();
self.builder = self.builder.listen(
format!("actix-web-service-{}", addr),
@ -252,11 +286,20 @@ where
c.host.clone().unwrap_or_else(|| format!("{}", addr)),
);
HttpService::build()
let svc = HttpService::build()
.keep_alive(c.keep_alive)
.client_timeout(c.client_timeout)
.local_addr(addr)
.finish(map_config(factory(), move |_| cfg.clone()))
.local_addr(addr);
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| {
(handler)(io as &dyn Any, ext)
})
} else {
svc
};
svc.finish(map_config(factory(), move |_| cfg.clone()))
.tcp()
},
)?;
@ -289,6 +332,8 @@ where
scheme: "https",
});
let on_connect_fn = self.on_connect_fn.clone();
self.builder = self.builder.listen(
format!("actix-web-service-{}", addr),
lst,
@ -299,11 +344,21 @@ where
addr,
c.host.clone().unwrap_or_else(|| format!("{}", addr)),
);
HttpService::build()
let svc = HttpService::build()
.keep_alive(c.keep_alive)
.client_timeout(c.client_timeout)
.client_disconnect(c.client_shutdown)
.finish(map_config(factory(), move |_| cfg.clone()))
.client_disconnect(c.client_shutdown);
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| {
(&*handler)(io as &dyn Any, ext)
})
} else {
svc
};
svc.finish(map_config(factory(), move |_| cfg.clone()))
.openssl(acceptor.clone())
},
)?;
@ -336,6 +391,8 @@ where
scheme: "https",
});
let on_connect_fn = self.on_connect_fn.clone();
self.builder = self.builder.listen(
format!("actix-web-service-{}", addr),
lst,
@ -346,11 +403,21 @@ where
addr,
c.host.clone().unwrap_or_else(|| format!("{}", addr)),
);
HttpService::build()
let svc = HttpService::build()
.keep_alive(c.keep_alive)
.client_timeout(c.client_timeout)
.client_disconnect(c.client_shutdown)
.finish(map_config(factory(), move |_| cfg.clone()))
.client_disconnect(c.client_shutdown);
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| {
(handler)(io as &dyn Any, ext)
})
} else {
svc
};
svc.finish(map_config(factory(), move |_| cfg.clone()))
.rustls(config.clone())
},
)?;
@ -441,7 +508,7 @@ where
}
#[cfg(unix)]
/// Start listening for unix domain connections on existing listener.
/// Start listening for unix domain (UDS) connections on existing listener.
pub fn listen_uds(
mut self,
lst: std::os::unix::net::UnixListener,
@ -460,6 +527,7 @@ where
});
let addr = format!("actix-web-service-{:?}", lst.local_addr()?);
let on_connect_fn = self.on_connect_fn.clone();
self.builder = self.builder.listen_uds(addr, lst, move || {
let c = cfg.lock().unwrap();
@ -468,11 +536,23 @@ where
socket_addr,
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
);
pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))).and_then(
HttpService::build()
.keep_alive(c.keep_alive)
.client_timeout(c.client_timeout)
.finish(map_config(factory(), move |_| config.clone())),
{
let svc = HttpService::build()
.keep_alive(c.keep_alive)
.client_timeout(c.client_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| {
(&*handler)(io as &dyn Any, ext)
})
} else {
svc
};
svc.finish(map_config(factory(), move |_| config.clone()))
},
)
})?;
Ok(self)

View File

@ -283,7 +283,7 @@ impl JsonConfig {
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or_else(|| &DEFAULT_CONFIG)
.unwrap_or(&DEFAULT_CONFIG)
}
}

View File

@ -284,7 +284,7 @@ impl PayloadConfig {
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or_else(|| &DEFAULT_CONFIG)
.unwrap_or(&DEFAULT_CONFIG)
}
}

View File

@ -4,7 +4,7 @@ use actix_router::IntoPattern;
use std::future::Future;
pub use actix_http::Response as HttpResponse;
pub use bytes::{Bytes, BytesMut};
pub use bytes::{Buf, BufMut, Bytes, BytesMut};
pub use futures_channel::oneshot::Canceled;
use crate::error::BlockingError;
@ -19,6 +19,7 @@ use crate::service::WebService;
pub use crate::config::ServiceConfig;
pub use crate::data::Data;
pub use crate::request::HttpRequest;
pub use crate::request_data::ReqData;
pub use crate::types::*;
/// Create resource for a specific path.

View File

@ -3,6 +3,7 @@
## Unreleased - 2020-xx-xx
* add ability to set address for `TestServer` [#1645]
* Upgrade `base64` to `0.13`.
[#1645]: https://github.com/actix/actix-web/pull/1645

View File

@ -38,7 +38,7 @@ actix-server = "1.0.0"
actix-testing = "1.0.0"
awc = "2.0.0"
base64 = "0.12"
base64 = "0.13"
bytes = "0.5.3"
futures-core = { version = "0.3.5", default-features = false }
http = "0.2.0"