mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-03 09:36:36 +02:00
Compare commits
33 Commits
files-v0.6
...
files-v0.6
Author | SHA1 | Date | |
---|---|---|---|
5469b02638 | |||
a66cd38ec5 | |||
20609e93fd | |||
bf282472ab | |||
7f4b44c258 | |||
66243717b3 | |||
102720d398 | |||
c3c7eb8df9 | |||
21f57caf4a | |||
47f5faf26e | |||
9777653dc0 | |||
9fde5b30db | |||
fd412a8223 | |||
cd511affd5 | |||
3200de3f34 | |||
b3e84b5c4b | |||
a3416112a5 | |||
21a08ca796 | |||
a9f497d05f | |||
cc9ba162f7 | |||
37799df978 | |||
0d93a8c273 | |||
3ae4f0a629 | |||
14a4f325d3 | |||
1bd2076b35 | |||
5454699bab | |||
d7c5c966d2 | |||
50894e392e | |||
008753f07a | |||
c92aa31f91 | |||
c25dd23820 | |||
acacb90b2e | |||
8459f566a8 |
18
.github/workflows/clippy-fmt.yml
vendored
18
.github/workflows/clippy-fmt.yml
vendored
@ -46,3 +46,21 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --workspace --tests --examples --all-features
|
||||
|
||||
lint-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rust-docs
|
||||
- name: Check for broken intra-doc links
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
with:
|
||||
command: doc
|
||||
args: --no-deps --all-features --workspace
|
||||
|
12
CHANGES.md
12
CHANGES.md
@ -3,6 +3,18 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 4.0.0-rc.1 - 2022-01-31
|
||||
### Changed
|
||||
- Rename `HttpServer::{client_timeout => client_request_timeout}`. [#2611]
|
||||
- Rename `HttpServer::{client_shutdown => client_disconnect_timeout}`. [#2611]
|
||||
|
||||
### Removed
|
||||
- `impl Future for HttpResponse`. [#2601]
|
||||
|
||||
[#2601]: https://github.com/actix/actix-web/pull/2601
|
||||
[#2611]: https://github.com/actix/actix-web/pull/2611
|
||||
|
||||
|
||||
## 4.0.0-beta.21 - 2022-01-21
|
||||
### Added
|
||||
- `HttpResponse::add_removal_cookie`. [#2586]
|
||||
|
14
Cargo.toml
14
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.0.0-beta.21"
|
||||
version = "4.0.0-rc.1"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -80,9 +80,9 @@ actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||
|
||||
actix-http = "3.0.0-beta.19"
|
||||
actix-router = "0.5.0-rc.2"
|
||||
actix-web-codegen = "0.5.0-rc.1"
|
||||
actix-http = { version = "3.0.0-rc.1", features = ["http2", "ws"] }
|
||||
actix-router = "0.5.0-rc.3"
|
||||
actix-web-codegen = "0.5.0-rc.2"
|
||||
|
||||
ahash = "0.7"
|
||||
bytes = "1"
|
||||
@ -108,9 +108,9 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||
url = "2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-files = "0.6.0-beta.15"
|
||||
actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.19", features = ["openssl"] }
|
||||
actix-files = "0.6.0-beta.16"
|
||||
actix-test = { version = "0.1.0-beta.12", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.20", features = ["openssl"] }
|
||||
|
||||
brotli = "3.3.3"
|
||||
const-str = "0.3"
|
||||
|
34
README.md
34
README.md
@ -6,10 +6,10 @@
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.21)
|
||||
[](https://docs.rs/actix-web/4.0.0-rc.1)
|
||||

|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.21)
|
||||
[](https://deps.rs/crate/actix-web/4.0.0-rc.1)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
@ -21,12 +21,13 @@
|
||||
|
||||
## Features
|
||||
|
||||
- Supports *HTTP/1.x* and *HTTP/2*
|
||||
- Supports _HTTP/1.x_ and _HTTP/2_
|
||||
- Streaming and pipelining
|
||||
- Powerful [request routing](https://actix.rs/docs/url-dispatch/) with optional macros
|
||||
- Full [Tokio](https://tokio.rs) compatibility
|
||||
- Keep-alive and slow requests handling
|
||||
- Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
||||
- Transparent content compression/decompression (br, gzip, deflate, zstd)
|
||||
- Powerful [request routing](https://actix.rs/docs/url-dispatch/)
|
||||
- Multipart streams
|
||||
- Static assets
|
||||
- SSL support using OpenSSL or Rustls
|
||||
@ -47,7 +48,7 @@ Dependencies:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-web = "3"
|
||||
actix-web = "4.0.0-rc.1"
|
||||
```
|
||||
|
||||
Code:
|
||||
@ -56,14 +57,15 @@ Code:
|
||||
use actix_web::{get, web, App, HttpServer, Responder};
|
||||
|
||||
#[get("/{id}/{name}/index.html")]
|
||||
async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder {
|
||||
async fn index(params: web::Path<(u32, String)>) -> impl Responder {
|
||||
let (id, name) = params.into_inner();
|
||||
format!("Hello {}! id:{}", name, id)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
#[actix_web::main] // or #[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(|| App::new().service(index))
|
||||
.bind("127.0.0.1:8080")?
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
@ -84,24 +86,18 @@ async fn main() -> std::io::Result<()> {
|
||||
- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
|
||||
- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
|
||||
|
||||
You may consider checking out
|
||||
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
||||
You may consider checking out [this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
One of the fastest web frameworks available according to the
|
||||
[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
|
||||
One of the fastest web frameworks available according to the [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under either of
|
||||
This project is licensed under either of the following licenses, at your option:
|
||||
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
[http://www.apache.org/licenses/LICENSE-2.0])
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||
[http://opensource.org/licenses/MIT])
|
||||
|
||||
at your option.
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0])
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT])
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
|
@ -3,6 +3,10 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.6.0-beta.16 - 2022-01-31
|
||||
- No significant changes since `0.6.0-beta.15`.
|
||||
|
||||
|
||||
## 0.6.0-beta.15 - 2022-01-21
|
||||
- No significant changes since `0.6.0-beta.14`.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.0-beta.15"
|
||||
version = "0.6.0-beta.16"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
@ -22,10 +22,10 @@ path = "src/lib.rs"
|
||||
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.0.0-beta.19"
|
||||
actix-http = "3.0.0-rc.1"
|
||||
actix-service = "2"
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4.0.0-beta.21", default-features = false }
|
||||
actix-web = { version = "4.0.0-rc.1", default-features = false }
|
||||
|
||||
askama_escape = "0.10"
|
||||
bitflags = "1"
|
||||
@ -43,6 +43,6 @@ tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.11"
|
||||
actix-web = "4.0.0-beta.21"
|
||||
actix-test = "0.1.0-beta.12"
|
||||
actix-web = "4.0.0-rc.1"
|
||||
tempfile = "3.2"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Static file serving for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.15)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.16)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.15)
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.16)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -37,7 +37,7 @@ use crate::{
|
||||
/// .service(Files::new("/static", "."));
|
||||
/// ```
|
||||
pub struct Files {
|
||||
path: String,
|
||||
mount_path: String,
|
||||
directory: PathBuf,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
@ -68,7 +68,7 @@ impl Clone for Files {
|
||||
default: self.default.clone(),
|
||||
renderer: self.renderer.clone(),
|
||||
file_flags: self.file_flags,
|
||||
path: self.path.clone(),
|
||||
mount_path: self.mount_path.clone(),
|
||||
mime_override: self.mime_override.clone(),
|
||||
path_filter: self.path_filter.clone(),
|
||||
use_guards: self.use_guards.clone(),
|
||||
@ -107,7 +107,7 @@ impl Files {
|
||||
};
|
||||
|
||||
Files {
|
||||
path: mount_path.trim_end_matches('/').to_owned(),
|
||||
mount_path: mount_path.trim_end_matches('/').to_owned(),
|
||||
directory: dir,
|
||||
index: None,
|
||||
show_index: false,
|
||||
@ -342,9 +342,9 @@ impl HttpServiceFactory for Files {
|
||||
}
|
||||
|
||||
let rdef = if config.is_root() {
|
||||
ResourceDef::root_prefix(&self.path)
|
||||
ResourceDef::root_prefix(&self.mount_path)
|
||||
} else {
|
||||
ResourceDef::prefix(&self.path)
|
||||
ResourceDef::prefix(&self.mount_path)
|
||||
};
|
||||
|
||||
config.register_service(rdef, guards, self, None)
|
||||
|
@ -106,7 +106,7 @@ mod tests {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ mod tests {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ mod tests {
|
||||
.insert_header((header::IF_NONE_MATCH, "miss_etag"))
|
||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ mod tests {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
@ -155,7 +155,7 @@ mod tests {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/x-toml"
|
||||
@ -196,7 +196,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||
"inline; filename=\"Cargo.toml\""
|
||||
@ -207,7 +207,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.disable_content_disposition();
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none());
|
||||
}
|
||||
|
||||
@ -235,7 +235,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/x-toml"
|
||||
@ -261,7 +261,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/xml"
|
||||
@ -284,7 +284,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"image/png"
|
||||
@ -300,7 +300,7 @@ mod tests {
|
||||
let file = NamedFile::open_async("tests/test.js").await.unwrap();
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"application/javascript; charset=utf-8"
|
||||
@ -330,7 +330,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"image/png"
|
||||
@ -353,7 +353,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"application/octet-stream"
|
||||
@ -379,7 +379,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/x-toml"
|
||||
@ -633,7 +633,7 @@ mod tests {
|
||||
async fn test_named_file_allowed_method() {
|
||||
let req = TestRequest::default().method(Method::GET).to_http_request();
|
||||
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
|
@ -209,6 +209,7 @@ impl NamedFile {
|
||||
Self::from_file(file, path)
|
||||
}
|
||||
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
/// Attempts to open a file asynchronously in read-only mode.
|
||||
///
|
||||
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
|
||||
@ -298,9 +299,11 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set content encoding for serving this file
|
||||
/// Sets content encoding for this file.
|
||||
///
|
||||
/// Must be used with [`actix_web::middleware::Compress`] to take effect.
|
||||
/// This prevents the `Compress` middleware from modifying the file contents and signals to
|
||||
/// browsers/clients how to decode it. For example, if serving a compressed HTML file (e.g.,
|
||||
/// `index.html.gz`) then use `.set_content_encoding(ContentEncoding::Gzip)`.
|
||||
#[inline]
|
||||
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
||||
self.encoding = Some(enc);
|
||||
|
@ -59,6 +59,8 @@ impl PathBufWrap {
|
||||
continue;
|
||||
} else if cfg!(windows) && segment.contains('\\') {
|
||||
return Err(UriSegmentError::BadChar('\\'));
|
||||
} else if cfg!(windows) && segment.contains(':') {
|
||||
return Err(UriSegmentError::BadChar(':'));
|
||||
} else {
|
||||
buf.push(segment)
|
||||
}
|
||||
@ -66,7 +68,11 @@ impl PathBufWrap {
|
||||
|
||||
// make sure we agree with stdlib parser
|
||||
for (i, component) in buf.components().enumerate() {
|
||||
assert!(matches!(component, Component::Normal(_)));
|
||||
assert!(
|
||||
matches!(component, Component::Normal(_)),
|
||||
"component `{:?}` is not normal",
|
||||
component
|
||||
);
|
||||
assert!(i < segment_count);
|
||||
}
|
||||
|
||||
@ -159,4 +165,26 @@ mod tests {
|
||||
PathBuf::from_iter(vec!["etc/passwd"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, should_panic)]
|
||||
fn windows_drive_traversal() {
|
||||
// detect issues in windows that could lead to path traversal
|
||||
// see <https://github.com/SergioBenitez/Rocket/issues/1949
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("C:test.txt", false).unwrap().0,
|
||||
PathBuf::from_iter(vec!["C:test.txt"])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("C:../whatever", false).unwrap().0,
|
||||
PathBuf::from_iter(vec!["C:../whatever"])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path(":test.txt", false).unwrap().0,
|
||||
PathBuf::from_iter(vec![":test.txt"])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||
}
|
||||
}
|
||||
None if this.show_index => Ok(this.show_index(req, path)),
|
||||
_ => Ok(ServiceResponse::from_err(
|
||||
None => Ok(ServiceResponse::from_err(
|
||||
FilesError::IsDirectory,
|
||||
req.into_parts().0,
|
||||
)),
|
||||
|
@ -3,6 +3,10 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.12 - 2022-01-31
|
||||
- No significant changes since `3.0.0-beta.11`.
|
||||
|
||||
|
||||
## 3.0.0-beta.11 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0-beta.11"
|
||||
version = "3.0.0-beta.12"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -35,7 +35,7 @@ actix-tls = "3.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-rt = "2.2"
|
||||
actix-server = "2"
|
||||
awc = { version = "3.0.0-beta.19", default-features = false }
|
||||
awc = { version = "3.0.0-beta.20", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4.0.0-beta.21", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.19"
|
||||
actix-web = { version = "4.0.0-rc.1", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-rc.1"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/3.0.0-beta.11)
|
||||
[](https://docs.rs/actix-http-test/3.0.0-beta.12)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br>
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.11)
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.12)
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -3,6 +3,38 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-rc.1 - 2022-01-31
|
||||
### Added
|
||||
- Implement `Default` for `KeepAlive`. [#2611]
|
||||
- Implement `From<Duration>` for `KeepAlive`. [#2611]
|
||||
- Implement `From<Option<Duration>>` for `KeepAlive`. [#2611]
|
||||
- Implement `Default` for `HttpServiceBuilder`. [#2611]
|
||||
- Crate `ws` feature flag, disabled by default. [#2618]
|
||||
- Crate `http2` feature flag, disabled by default. [#2618]
|
||||
|
||||
### Changed
|
||||
- Rename `ServiceConfig::{client_timer_expire => client_request_deadline}`. [#2611]
|
||||
- Rename `ServiceConfig::{client_disconnect_timer => client_disconnect_deadline}`. [#2611]
|
||||
- Deadline methods in `ServiceConfig` now return `std::time::Instant`s instead of Tokio's wrapper type. [#2611]
|
||||
- Rename `h1::Codec::{keepalive => keep_alive}`. [#2611]
|
||||
- Rename `h1::Codec::{keepalive_enabled => keep_alive_enabled}`. [#2611]
|
||||
- Rename `h1::ClientCodec::{keepalive => keep_alive}`. [#2611]
|
||||
- Rename `h1::ClientPayloadCodec::{keepalive => keep_alive}`. [#2611]
|
||||
- `ServiceConfig::keep_alive` now returns a `KeepAlive`. [#2611]
|
||||
|
||||
### Fixed
|
||||
- HTTP/1.1 dispatcher correctly uses client request timeout. [#2611]
|
||||
|
||||
### Removed
|
||||
- `ServiceConfig::{client_timer, keep_alive_timer}`. [#2611]
|
||||
- `impl From<usize> for KeepAlive`; use `Duration`s instead. [#2611]
|
||||
- `impl From<Option<usize>> for KeepAlive`; use `Duration`s instead. [#2611]
|
||||
- `HttpServiceBuilder::new`; use `default` instead. [#2611]
|
||||
|
||||
[#2611]: https://github.com/actix/actix-web/pull/2611
|
||||
[#2618]: https://github.com/actix/actix-web/pull/2618
|
||||
|
||||
|
||||
## 3.0.0-beta.19 - 2022-01-21
|
||||
### Added
|
||||
- Response headers can be sent as camel case using `res.head_mut().set_camel_case_headers(true)`. [#2587]
|
||||
@ -416,6 +448,13 @@
|
||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||
|
||||
|
||||
## 2.2.2 - 2022-01-21
|
||||
### Changed
|
||||
- Migrate to `brotli` crate. [ad7e3c06]
|
||||
|
||||
[ad7e3c06]: https://github.com/actix/actix-web/commit/ad7e3c06
|
||||
|
||||
|
||||
## 2.2.1 - 2021-08-09
|
||||
### Fixed
|
||||
- Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.0.0-beta.19"
|
||||
version = "3.0.0-rc.1"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -29,69 +29,85 @@ path = "src/lib.rs"
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# openssl
|
||||
# HTTP/2 protocol support
|
||||
http2 = ["h2"]
|
||||
|
||||
# WebSocket protocol implementation
|
||||
ws = [
|
||||
"local-channel",
|
||||
"base64",
|
||||
"rand",
|
||||
"sha-1",
|
||||
]
|
||||
|
||||
# TLS via OpenSSL
|
||||
openssl = ["actix-tls/accept", "actix-tls/openssl"]
|
||||
|
||||
# rustls support
|
||||
# TLS via Rustls
|
||||
rustls = ["actix-tls/accept", "actix-tls/rustls"]
|
||||
|
||||
# enable compression support
|
||||
compress-brotli = ["brotli", "__compress"]
|
||||
compress-gzip = ["flate2", "__compress"]
|
||||
compress-zstd = ["zstd", "__compress"]
|
||||
# Compression codecs
|
||||
compress-brotli = ["__compress", "brotli"]
|
||||
compress-gzip = ["__compress", "flate2"]
|
||||
compress-zstd = ["__compress", "zstd"]
|
||||
|
||||
# Internal (PRIVATE!) features used to aid testing and cheking feature status.
|
||||
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||
# Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime.
|
||||
__compress = []
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2.0.0"
|
||||
actix-service = "2"
|
||||
actix-codec = "0.4.1"
|
||||
actix-utils = "3.0.0"
|
||||
actix-utils = "3"
|
||||
actix-rt = { version = "2.2", default-features = false }
|
||||
|
||||
ahash = "0.7"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.2"
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
h2 = "0.3.9"
|
||||
http = "0.2.5"
|
||||
httparse = "1.5.1"
|
||||
httpdate = "1.0.1"
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
local-channel = "0.1"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2"
|
||||
rand = "0.8"
|
||||
sha-1 = "0.10"
|
||||
smallvec = "1.6.1"
|
||||
|
||||
# tls
|
||||
# http2
|
||||
h2 = { version = "0.3.9", optional = true }
|
||||
|
||||
# websockets
|
||||
local-channel = { version = "0.1", optional = true }
|
||||
base64 = { version = "0.13", optional = true }
|
||||
rand = { version = "0.8", optional = true }
|
||||
sha-1 = { version = "0.10", optional = true }
|
||||
|
||||
# openssl/rustls
|
||||
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||
|
||||
# compression
|
||||
# compress-*
|
||||
brotli = { version = "3.3.3", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
zstd = { version = "0.9", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
|
||||
actix-http-test = { version = "3.0.0-beta.12", features = ["openssl"] }
|
||||
actix-server = "2"
|
||||
actix-tls = { version = "3.0.0", features = ["openssl"] }
|
||||
actix-web = "4.0.0-beta.21"
|
||||
actix-web = "4.0.0-rc.1"
|
||||
|
||||
async-stream = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
memchr = "2.4"
|
||||
once_cell = "1.9"
|
||||
rcgen = "0.8"
|
||||
regex = "1.3"
|
||||
rustls-pemfile = "0.2"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.19)
|
||||
[](https://docs.rs/actix-http/3.0.0-rc.1)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.19)
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-rc.1)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
27
actix-http/examples/bench.rs
Normal file
27
actix-http/examples/bench.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::{convert::Infallible, io, time::Duration};
|
||||
|
||||
use actix_http::{HttpService, Request, Response, StatusCode};
|
||||
use actix_server::Server;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static STR: Lazy<String> = Lazy::new(|| "HELLO WORLD ".repeat(20));
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
Server::build()
|
||||
.bind("dispatcher-benchmark", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.finish(|_: Request| async move {
|
||||
let mut res = Response::build(StatusCode::OK);
|
||||
Ok::<_, Infallible>(res.body(&**STR))
|
||||
})
|
||||
.tcp()
|
||||
})?
|
||||
// limiting number of workers so that bench client is not sharing as many resources
|
||||
.workers(4)
|
||||
.run()
|
||||
.await
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use std::io;
|
||||
use std::{io, time::Duration};
|
||||
|
||||
use actix_http::{Error, HttpService, Request, Response, StatusCode};
|
||||
use actix_server::Server;
|
||||
@ -13,8 +13,8 @@ async fn main() -> io::Result<()> {
|
||||
Server::build()
|
||||
.bind("echo", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
// handles HTTP/1.1 and HTTP/2
|
||||
.finish(|mut req: Request| async move {
|
||||
let mut body = BytesMut::new();
|
||||
|
25
actix-http/examples/h2spec.rs
Normal file
25
actix-http/examples/h2spec.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use std::{convert::Infallible, io};
|
||||
|
||||
use actix_http::{HttpService, Request, Response, StatusCode};
|
||||
use actix_server::Server;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static STR: Lazy<String> = Lazy::new(|| "HELLO WORLD ".repeat(100));
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
Server::build()
|
||||
.bind("h2spec", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.h2(|_: Request| async move {
|
||||
let mut res = Response::build(StatusCode::OK);
|
||||
Ok::<_, Infallible>(res.body(&**STR))
|
||||
})
|
||||
.tcp()
|
||||
})?
|
||||
.workers(4)
|
||||
.run()
|
||||
.await
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use std::{convert::Infallible, io};
|
||||
use std::{convert::Infallible, io, time::Duration};
|
||||
|
||||
use actix_http::{
|
||||
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
|
||||
@ -12,8 +12,8 @@ async fn main() -> io::Result<()> {
|
||||
Server::build()
|
||||
.bind("hello-world", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
.on_connect_ext(|_, ext| {
|
||||
ext.insert(42u32);
|
||||
})
|
||||
|
@ -31,7 +31,7 @@ impl fmt::Debug for BoxBodyInner {
|
||||
}
|
||||
|
||||
impl BoxBody {
|
||||
/// Same as `MessageBody::boxed`.
|
||||
/// Boxes body type, erasing type information.
|
||||
///
|
||||
/// If the body type to wrap is unknown or generic it is better to use [`MessageBody::boxed`] to
|
||||
/// avoid double boxing.
|
||||
|
@ -14,8 +14,44 @@ use pin_project_lite::pin_project;
|
||||
|
||||
use super::{BodySize, BoxBody};
|
||||
|
||||
/// An interface types that can converted to bytes and used as response bodies.
|
||||
// TODO: examples
|
||||
/// An interface for types that can be used as a response body.
|
||||
///
|
||||
/// It is not usually necessary to create custom body types, this trait is already [implemented for
|
||||
/// a large number of sensible body types](#foreign-impls) including:
|
||||
/// - Empty body: `()`
|
||||
/// - Text-based: `String`, `&'static str`, `ByteString`.
|
||||
/// - Byte-based: `Bytes`, `BytesMut`, `Vec<u8>`, `&'static [u8]`;
|
||||
/// - Streams: [`BodyStream`](super::BodyStream), [`SizedStream`](super::SizedStream)
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::convert::Infallible;
|
||||
/// # use std::task::{Poll, Context};
|
||||
/// # use std::pin::Pin;
|
||||
/// # use bytes::Bytes;
|
||||
/// # use actix_http::body::{BodySize, MessageBody};
|
||||
/// struct Repeat {
|
||||
/// chunk: String,
|
||||
/// n_times: usize,
|
||||
/// }
|
||||
///
|
||||
/// impl MessageBody for Repeat {
|
||||
/// type Error = Infallible;
|
||||
///
|
||||
/// fn size(&self) -> BodySize {
|
||||
/// BodySize::Sized((self.chunk.len() * self.n_times) as u64)
|
||||
/// }
|
||||
///
|
||||
/// fn poll_next(
|
||||
/// self: Pin<&mut Self>,
|
||||
/// _cx: &mut Context<'_>,
|
||||
/// ) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
/// let payload_string = self.chunk.repeat(self.n_times);
|
||||
/// let payload_bytes = Bytes::from(payload_string);
|
||||
/// Poll::Ready(Some(Ok(payload_bytes)))
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait MessageBody {
|
||||
/// The type of error that will be returned if streaming body fails.
|
||||
///
|
||||
@ -29,7 +65,22 @@ pub trait MessageBody {
|
||||
fn size(&self) -> BodySize;
|
||||
|
||||
/// Attempt to pull out the next chunk of body bytes.
|
||||
// TODO: expand documentation
|
||||
///
|
||||
/// # Return Value
|
||||
/// Similar to the `Stream` interface, there are several possible return values, each indicating
|
||||
/// a distinct state:
|
||||
/// - `Poll::Pending` means that this body's next chunk is not ready yet. Implementations must
|
||||
/// ensure that the current task will be notified when the next chunk may be ready.
|
||||
/// - `Poll::Ready(Some(val))` means that the body has successfully produced a chunk, `val`,
|
||||
/// and may produce further values on subsequent `poll_next` calls.
|
||||
/// - `Poll::Ready(None)` means that the body is complete, and `poll_next` should not be
|
||||
/// invoked again.
|
||||
///
|
||||
/// # Panics
|
||||
/// Once a body is complete (i.e., `poll_next` returned `Ready(None)`), calling its `poll_next`
|
||||
/// method again may panic, block forever, or cause other kinds of problems; this trait places
|
||||
/// no requirements on the effects of such a call. However, as the `poll_next` method is not
|
||||
/// marked unsafe, Rust’s usual rules apply: calls must never cause UB, regardless of its state.
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
@ -37,7 +88,7 @@ pub trait MessageBody {
|
||||
|
||||
/// Try to convert into the complete chunk of body bytes.
|
||||
///
|
||||
/// Implement this method if the entire body can be trivially extracted. This is useful for
|
||||
/// Override this method if the complete body can be trivially extracted. This is useful for
|
||||
/// optimizations where `poll_next` calls can be avoided.
|
||||
///
|
||||
/// Body types with [`BodySize::None`] are allowed to return empty `Bytes`. Although, if calling
|
||||
@ -54,7 +105,11 @@ pub trait MessageBody {
|
||||
Err(self)
|
||||
}
|
||||
|
||||
/// Converts this body into `BoxBody`.
|
||||
/// Wraps this body into a `BoxBody`.
|
||||
///
|
||||
/// No-op when called on a `BoxBody`, meaning there is no risk of double boxing when calling
|
||||
/// this on a generic `MessageBody`. Prefer this over [`BoxBody::new`] when a boxed body
|
||||
/// is required.
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody
|
||||
where
|
||||
|
@ -1,4 +1,9 @@
|
||||
//! Traits and structures to aid consuming and writing HTTP payloads.
|
||||
//!
|
||||
//! "Body" and "payload" are used somewhat interchangeably in this documentation.
|
||||
|
||||
// Though the spec kinda reads like "payload" is the possibly-transfer-encoded part of the message
|
||||
// and the "body" is the intended possibly-decoded version of that.
|
||||
|
||||
mod body_stream;
|
||||
mod boxed;
|
||||
|
@ -10,9 +10,12 @@ use super::{BodySize, MessageBody};
|
||||
|
||||
/// Body type for responses that forbid payloads.
|
||||
///
|
||||
/// Distinct from an empty response which would contain a Content-Length header.
|
||||
///
|
||||
/// This is distinct from an "empty" response which _would_ contain a `Content-Length` header.
|
||||
/// For an "empty" body, use `()` or `Bytes::new()`.
|
||||
///
|
||||
/// For example, the HTTP spec forbids a payload to be sent with a `204 No Content` response.
|
||||
/// In this case, the payload (or lack thereof) is implicit from the status code, so a
|
||||
/// `Content-Length` header is not required.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct None;
|
||||
|
@ -1,25 +1,22 @@
|
||||
use std::{fmt, marker::PhantomData, net, rc::Rc};
|
||||
use std::{fmt, marker::PhantomData, net, rc::Rc, time::Duration};
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
config::{KeepAlive, ServiceConfig},
|
||||
h1::{self, ExpectHandler, H1Service, UpgradeHandler},
|
||||
h2::H2Service,
|
||||
service::HttpService,
|
||||
ConnectCallback, Extensions, Request, Response,
|
||||
ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfig,
|
||||
};
|
||||
|
||||
/// A HTTP service builder
|
||||
/// An HTTP service builder.
|
||||
///
|
||||
/// This type can be used to construct an instance of [`HttpService`] through a
|
||||
/// builder-like pattern.
|
||||
/// This type can construct an instance of [`HttpService`] through a builder-like pattern.
|
||||
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
||||
keep_alive: KeepAlive,
|
||||
client_timeout: u64,
|
||||
client_disconnect: u64,
|
||||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
expect: X,
|
||||
@ -28,22 +25,23 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
||||
_phantom: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||
impl<T, S> Default for HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
/// Create instance of `ServiceConfigBuilder`
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
fn default() -> Self {
|
||||
HttpServiceBuilder {
|
||||
keep_alive: KeepAlive::Timeout(5),
|
||||
client_timeout: 5000,
|
||||
client_disconnect: 0,
|
||||
// ServiceConfig parts (make sure defaults match)
|
||||
keep_alive: KeepAlive::default(),
|
||||
client_request_timeout: Duration::from_secs(5),
|
||||
client_disconnect_timeout: Duration::ZERO,
|
||||
secure: false,
|
||||
local_addr: None,
|
||||
|
||||
// dispatcher parts
|
||||
expect: ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect_ext: None,
|
||||
@ -65,9 +63,11 @@ where
|
||||
U::Error: fmt::Display,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Set server keep-alive setting.
|
||||
/// Set connection keep-alive setting.
|
||||
///
|
||||
/// By default keep alive is set to a 5 seconds.
|
||||
/// Applies to HTTP/1.1 keep-alive and HTTP/2 ping-pong.
|
||||
///
|
||||
/// By default keep-alive is 5 seconds.
|
||||
pub fn keep_alive<W: Into<KeepAlive>>(mut self, val: W) -> Self {
|
||||
self.keep_alive = val.into();
|
||||
self
|
||||
@ -85,33 +85,45 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Set server client timeout in milliseconds for first request.
|
||||
/// Set client request timeout (for first request).
|
||||
///
|
||||
/// Defines a timeout for reading client request header. If a client does not transmit
|
||||
/// the entire set headers within this time, the request is terminated with
|
||||
/// the 408 (Request Time-out) error.
|
||||
/// Defines a timeout for reading client request header. If the client does not transmit the
|
||||
/// request head within this duration, the connection is terminated with a `408 Request Timeout`
|
||||
/// response error.
|
||||
///
|
||||
/// To disable timeout set value to 0.
|
||||
/// A duration of zero disables the timeout.
|
||||
///
|
||||
/// By default client timeout is set to 5000 milliseconds.
|
||||
pub fn client_timeout(mut self, val: u64) -> Self {
|
||||
self.client_timeout = val;
|
||||
/// By default, the client timeout is 5 seconds.
|
||||
pub fn client_request_timeout(mut self, dur: Duration) -> Self {
|
||||
self.client_request_timeout = dur;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set server connection disconnect timeout in milliseconds.
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "3.0.0", note = "Renamed to `client_request_timeout`.")]
|
||||
pub fn client_timeout(self, dur: Duration) -> Self {
|
||||
self.client_request_timeout(dur)
|
||||
}
|
||||
|
||||
/// Set client connection disconnect timeout.
|
||||
///
|
||||
/// Defines a timeout for disconnect connection. If a disconnect procedure does not complete
|
||||
/// within this time, the request get dropped. This timeout affects secure connections.
|
||||
///
|
||||
/// To disable timeout set value to 0.
|
||||
/// A duration of zero disables the timeout.
|
||||
///
|
||||
/// By default disconnect timeout is set to 0.
|
||||
pub fn client_disconnect(mut self, val: u64) -> Self {
|
||||
self.client_disconnect = val;
|
||||
/// By default, the disconnect timeout is disabled.
|
||||
pub fn client_disconnect_timeout(mut self, dur: Duration) -> Self {
|
||||
self.client_disconnect_timeout = dur;
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "3.0.0", note = "Renamed to `client_disconnect_timeout`.")]
|
||||
pub fn client_disconnect(self, dur: Duration) -> Self {
|
||||
self.client_disconnect_timeout(dur)
|
||||
}
|
||||
|
||||
/// Provide service for `EXPECT: 100-Continue` support.
|
||||
///
|
||||
/// Service get called with request that contains `EXPECT` header.
|
||||
@ -126,8 +138,8 @@ where
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
client_timeout: self.client_timeout,
|
||||
client_disconnect: self.client_disconnect,
|
||||
client_request_timeout: self.client_request_timeout,
|
||||
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||
secure: self.secure,
|
||||
local_addr: self.local_addr,
|
||||
expect: expect.into_factory(),
|
||||
@ -150,8 +162,8 @@ where
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
client_timeout: self.client_timeout,
|
||||
client_disconnect: self.client_disconnect,
|
||||
client_request_timeout: self.client_request_timeout,
|
||||
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||
secure: self.secure,
|
||||
local_addr: self.local_addr,
|
||||
expect: self.expect,
|
||||
@ -185,8 +197,8 @@ where
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_timeout,
|
||||
self.client_disconnect,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
@ -198,7 +210,8 @@ where
|
||||
}
|
||||
|
||||
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
||||
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
|
||||
#[cfg(feature = "http2")]
|
||||
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||
where
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
@ -209,13 +222,14 @@ where
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_timeout,
|
||||
self.client_disconnect,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
|
||||
H2Service::with_config(cfg, service.into_factory()).on_connect_ext(self.on_connect_ext)
|
||||
crate::h2::H2Service::with_config(cfg, service.into_factory())
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create `HttpService` instance.
|
||||
@ -230,8 +244,8 @@ where
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_timeout,
|
||||
self.client_disconnect,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
|
@ -1,71 +1,36 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
fmt::{self, Write},
|
||||
net,
|
||||
rc::Rc,
|
||||
time::{Duration, SystemTime},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use actix_rt::{
|
||||
task::JoinHandle,
|
||||
time::{interval, sleep_until, Instant, Sleep},
|
||||
};
|
||||
use bytes::BytesMut;
|
||||
|
||||
/// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||
pub(crate) const DATE_VALUE_LENGTH: usize = 29;
|
||||
use crate::{date::DateService, KeepAlive};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
/// Server keep-alive setting
|
||||
pub enum KeepAlive {
|
||||
/// Keep alive in seconds
|
||||
Timeout(usize),
|
||||
|
||||
/// Rely on OS to shutdown tcp connection
|
||||
Os,
|
||||
|
||||
/// Disabled
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl From<usize> for KeepAlive {
|
||||
fn from(keepalive: usize) -> Self {
|
||||
KeepAlive::Timeout(keepalive)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<usize>> for KeepAlive {
|
||||
fn from(keepalive: Option<usize>) -> Self {
|
||||
if let Some(keepalive) = keepalive {
|
||||
KeepAlive::Timeout(keepalive)
|
||||
} else {
|
||||
KeepAlive::Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Http service configuration
|
||||
/// HTTP service configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceConfig(Rc<Inner>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Inner {
|
||||
keep_alive: Option<Duration>,
|
||||
client_timeout: u64,
|
||||
client_disconnect: u64,
|
||||
ka_enabled: bool,
|
||||
keep_alive: KeepAlive,
|
||||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<std::net::SocketAddr>,
|
||||
date_service: DateService,
|
||||
}
|
||||
|
||||
impl Clone for ServiceConfig {
|
||||
fn clone(&self) -> Self {
|
||||
ServiceConfig(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServiceConfig {
|
||||
fn default() -> Self {
|
||||
Self::new(KeepAlive::Timeout(5), 0, 0, false, None)
|
||||
Self::new(
|
||||
KeepAlive::default(),
|
||||
Duration::from_secs(5),
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,34 +38,22 @@ impl ServiceConfig {
|
||||
/// Create instance of `ServiceConfig`
|
||||
pub fn new(
|
||||
keep_alive: KeepAlive,
|
||||
client_timeout: u64,
|
||||
client_disconnect: u64,
|
||||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
) -> ServiceConfig {
|
||||
let (keep_alive, ka_enabled) = match keep_alive {
|
||||
KeepAlive::Timeout(val) => (val as u64, true),
|
||||
KeepAlive::Os => (0, true),
|
||||
KeepAlive::Disabled => (0, false),
|
||||
};
|
||||
let keep_alive = if ka_enabled && keep_alive > 0 {
|
||||
Some(Duration::from_secs(keep_alive))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ServiceConfig(Rc::new(Inner {
|
||||
keep_alive,
|
||||
ka_enabled,
|
||||
client_timeout,
|
||||
client_disconnect,
|
||||
keep_alive: keep_alive.normalize(),
|
||||
client_request_timeout,
|
||||
client_disconnect_timeout,
|
||||
secure,
|
||||
local_addr,
|
||||
date_service: DateService::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns true if connection is secure (HTTPS)
|
||||
/// Returns `true` if connection is secure (i.e., using TLS / HTTPS).
|
||||
#[inline]
|
||||
pub fn secure(&self) -> bool {
|
||||
self.0.secure
|
||||
@ -114,239 +67,92 @@ impl ServiceConfig {
|
||||
self.0.local_addr
|
||||
}
|
||||
|
||||
/// Keep alive duration if configured.
|
||||
/// Connection keep-alive setting.
|
||||
#[inline]
|
||||
pub fn keep_alive(&self) -> Option<Duration> {
|
||||
pub fn keep_alive(&self) -> KeepAlive {
|
||||
self.0.keep_alive
|
||||
}
|
||||
|
||||
/// Return state of connection keep-alive functionality
|
||||
#[inline]
|
||||
pub fn keep_alive_enabled(&self) -> bool {
|
||||
self.0.ka_enabled
|
||||
}
|
||||
|
||||
/// Client timeout for first request.
|
||||
#[inline]
|
||||
pub fn client_timer(&self) -> Option<Sleep> {
|
||||
let delay_time = self.0.client_timeout;
|
||||
if delay_time != 0 {
|
||||
Some(sleep_until(self.now() + Duration::from_millis(delay_time)))
|
||||
} else {
|
||||
None
|
||||
/// Creates a time object representing the deadline for this connection's keep-alive period, if
|
||||
/// enabled.
|
||||
///
|
||||
/// When [`KeepAlive::Os`] or [`KeepAlive::Disabled`] is set, this will return `None`.
|
||||
pub fn keep_alive_deadline(&self) -> Option<Instant> {
|
||||
match self.keep_alive() {
|
||||
KeepAlive::Timeout(dur) => Some(self.now() + dur),
|
||||
KeepAlive::Os => None,
|
||||
KeepAlive::Disabled => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Client timeout for first request.
|
||||
pub fn client_timer_expire(&self) -> Option<Instant> {
|
||||
let delay = self.0.client_timeout;
|
||||
if delay != 0 {
|
||||
Some(self.now() + Duration::from_millis(delay))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
/// Creates a time object representing the deadline for the client to finish sending the head of
|
||||
/// its first request.
|
||||
///
|
||||
/// Returns `None` if this `ServiceConfig was` constructed with `client_request_timeout: 0`.
|
||||
pub fn client_request_deadline(&self) -> Option<Instant> {
|
||||
let timeout = self.0.client_request_timeout;
|
||||
(timeout != Duration::ZERO).then(|| self.now() + timeout)
|
||||
}
|
||||
|
||||
/// Client disconnect timer
|
||||
pub fn client_disconnect_timer(&self) -> Option<Instant> {
|
||||
let delay = self.0.client_disconnect;
|
||||
if delay != 0 {
|
||||
Some(self.now() + Duration::from_millis(delay))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
/// Creates a time object representing the deadline for the client to disconnect.
|
||||
pub fn client_disconnect_deadline(&self) -> Option<Instant> {
|
||||
let timeout = self.0.client_disconnect_timeout;
|
||||
(timeout != Duration::ZERO).then(|| self.now() + timeout)
|
||||
}
|
||||
|
||||
/// Return keep-alive timer delay is configured.
|
||||
#[inline]
|
||||
pub fn keep_alive_timer(&self) -> Option<Sleep> {
|
||||
self.keep_alive().map(|ka| sleep_until(self.now() + ka))
|
||||
}
|
||||
|
||||
/// Keep-alive expire time
|
||||
pub fn keep_alive_expire(&self) -> Option<Instant> {
|
||||
self.keep_alive().map(|ka| self.now() + ka)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn now(&self) -> Instant {
|
||||
self.0.date_service.now()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn set_date(&self, dst: &mut BytesMut, camel_case: bool) {
|
||||
pub(crate) fn write_date_header(&self, dst: &mut BytesMut, camel_case: bool) {
|
||||
let mut buf: [u8; 39] = [0; 39];
|
||||
|
||||
buf[..6].copy_from_slice(if camel_case { b"Date: " } else { b"date: " });
|
||||
|
||||
self.0
|
||||
.date_service
|
||||
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
|
||||
.with_date(|date| buf[6..35].copy_from_slice(&date.bytes));
|
||||
|
||||
buf[35..].copy_from_slice(b"\r\n\r\n");
|
||||
dst.extend_from_slice(&buf);
|
||||
}
|
||||
|
||||
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
|
||||
#[allow(unused)] // used with `http2` feature flag
|
||||
pub(crate) fn write_date_header_value(&self, dst: &mut BytesMut) {
|
||||
self.0
|
||||
.date_service
|
||||
.set_date(|date| dst.extend_from_slice(&date.bytes));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct Date {
|
||||
bytes: [u8; DATE_VALUE_LENGTH],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl Date {
|
||||
fn new() -> Date {
|
||||
let mut date = Date {
|
||||
bytes: [0; DATE_VALUE_LENGTH],
|
||||
pos: 0,
|
||||
};
|
||||
date.update();
|
||||
date
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.pos = 0;
|
||||
write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Write for Date {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
let len = s.len();
|
||||
self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes());
|
||||
self.pos += len;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Service for update Date and Instant periodically at 500 millis interval.
|
||||
struct DateService {
|
||||
current: Rc<Cell<(Date, Instant)>>,
|
||||
handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Drop for DateService {
|
||||
fn drop(&mut self) {
|
||||
// stop the timer update async task on drop.
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl DateService {
|
||||
fn new() -> Self {
|
||||
// shared date and timer for DateService and update async task.
|
||||
let current = Rc::new(Cell::new((Date::new(), Instant::now())));
|
||||
let current_clone = Rc::clone(¤t);
|
||||
// spawn an async task sleep for 500 milli and update current date/timer in a loop.
|
||||
// handle is used to stop the task on DateService drop.
|
||||
let handle = actix_rt::spawn(async move {
|
||||
#[cfg(test)]
|
||||
let _notify = notify_on_drop::NotifyOnDrop::new();
|
||||
|
||||
let mut interval = interval(Duration::from_millis(500));
|
||||
loop {
|
||||
let now = interval.tick().await;
|
||||
let date = Date::new();
|
||||
current_clone.set((date, now));
|
||||
}
|
||||
});
|
||||
|
||||
DateService { current, handle }
|
||||
}
|
||||
|
||||
fn now(&self) -> Instant {
|
||||
self.current.get().1
|
||||
}
|
||||
|
||||
fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
|
||||
f(&self.current.get().0);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move to a util module for testing all spawn handle drop style tasks.
|
||||
/// Test Module for checking the drop state of certain async tasks that are spawned
|
||||
/// with `actix_rt::spawn`
|
||||
///
|
||||
/// The target task must explicitly generate `NotifyOnDrop` when spawn the task
|
||||
#[cfg(test)]
|
||||
mod notify_on_drop {
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
/// Check if the spawned task is dropped.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics when there was no `NotifyOnDrop` instance on current thread.
|
||||
pub(crate) fn is_dropped() -> bool {
|
||||
NOTIFY_DROPPED.with(|bool| {
|
||||
bool.borrow()
|
||||
.expect("No NotifyOnDrop existed on current thread")
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) struct NotifyOnDrop;
|
||||
|
||||
impl NotifyOnDrop {
|
||||
/// # Panic:
|
||||
///
|
||||
/// When construct multiple instances on any given thread.
|
||||
pub(crate) fn new() -> Self {
|
||||
NOTIFY_DROPPED.with(|bool| {
|
||||
let mut bool = bool.borrow_mut();
|
||||
if bool.is_some() {
|
||||
panic!("NotifyOnDrop existed on current thread");
|
||||
} else {
|
||||
*bool = Some(false);
|
||||
}
|
||||
});
|
||||
|
||||
NotifyOnDrop
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NotifyOnDrop {
|
||||
fn drop(&mut self) {
|
||||
NOTIFY_DROPPED.with(|bool| {
|
||||
if let Some(b) = bool.borrow_mut().as_mut() {
|
||||
*b = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
.with_date(|date| dst.extend_from_slice(&date.bytes));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{date::DATE_VALUE_LENGTH, notify_on_drop};
|
||||
|
||||
use actix_rt::{task::yield_now, time::sleep};
|
||||
use actix_rt::{
|
||||
task::yield_now,
|
||||
time::{sleep, sleep_until},
|
||||
};
|
||||
use memchr::memmem;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_date_service_update() {
|
||||
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
|
||||
let settings =
|
||||
ServiceConfig::new(KeepAlive::Os, Duration::ZERO, Duration::ZERO, false, None);
|
||||
|
||||
yield_now().await;
|
||||
|
||||
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||
settings.set_date(&mut buf1, false);
|
||||
settings.write_date_header(&mut buf1, false);
|
||||
let now1 = settings.now();
|
||||
|
||||
sleep_until(Instant::now() + Duration::from_secs(2)).await;
|
||||
sleep_until((Instant::now() + Duration::from_secs(2)).into()).await;
|
||||
yield_now().await;
|
||||
|
||||
let now2 = settings.now();
|
||||
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||
settings.set_date(&mut buf2, false);
|
||||
settings.write_date_header(&mut buf2, false);
|
||||
|
||||
assert_ne!(now1, now2);
|
||||
|
||||
@ -402,10 +208,10 @@ mod tests {
|
||||
let settings = ServiceConfig::default();
|
||||
|
||||
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||
settings.set_date(&mut buf1, false);
|
||||
settings.write_date_header(&mut buf1, false);
|
||||
|
||||
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||
settings.set_date(&mut buf2, false);
|
||||
settings.write_date_header(&mut buf2, false);
|
||||
|
||||
assert_eq!(buf1, buf2);
|
||||
}
|
||||
@ -415,11 +221,11 @@ mod tests {
|
||||
let settings = ServiceConfig::default();
|
||||
|
||||
let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||
settings.set_date(&mut buf, false);
|
||||
settings.write_date_header(&mut buf, false);
|
||||
assert!(memmem::find(&buf, b"date:").is_some());
|
||||
|
||||
let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||
settings.set_date(&mut buf, true);
|
||||
settings.write_date_header(&mut buf, true);
|
||||
assert!(memmem::find(&buf, b"Date:").is_some());
|
||||
}
|
||||
}
|
||||
|
92
actix-http/src/date.rs
Normal file
92
actix-http/src/date.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
fmt::{self, Write},
|
||||
rc::Rc,
|
||||
time::{Duration, Instant, SystemTime},
|
||||
};
|
||||
|
||||
use actix_rt::{task::JoinHandle, time::interval};
|
||||
|
||||
/// "Thu, 01 Jan 1970 00:00:00 GMT".len()
|
||||
pub(crate) const DATE_VALUE_LENGTH: usize = 29;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct Date {
|
||||
pub(crate) bytes: [u8; DATE_VALUE_LENGTH],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl Date {
|
||||
fn new() -> Date {
|
||||
let mut date = Date {
|
||||
bytes: [0; DATE_VALUE_LENGTH],
|
||||
pos: 0,
|
||||
};
|
||||
date.update();
|
||||
date
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.pos = 0;
|
||||
write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Write for Date {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
let len = s.len();
|
||||
self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes());
|
||||
self.pos += len;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Service for update Date and Instant periodically at 500 millis interval.
|
||||
pub(crate) struct DateService {
|
||||
current: Rc<Cell<(Date, Instant)>>,
|
||||
handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl DateService {
|
||||
pub(crate) fn new() -> Self {
|
||||
// shared date and timer for DateService and update async task.
|
||||
let current = Rc::new(Cell::new((Date::new(), Instant::now())));
|
||||
let current_clone = Rc::clone(¤t);
|
||||
// spawn an async task sleep for 500 millis and update current date/timer in a loop.
|
||||
// handle is used to stop the task on DateService drop.
|
||||
let handle = actix_rt::spawn(async move {
|
||||
#[cfg(test)]
|
||||
let _notify = crate::notify_on_drop::NotifyOnDrop::new();
|
||||
|
||||
let mut interval = interval(Duration::from_millis(500));
|
||||
loop {
|
||||
let now = interval.tick().await;
|
||||
let date = Date::new();
|
||||
current_clone.set((date, now.into_std()));
|
||||
}
|
||||
});
|
||||
|
||||
DateService { current, handle }
|
||||
}
|
||||
|
||||
pub(crate) fn now(&self) -> Instant {
|
||||
self.current.get().1
|
||||
}
|
||||
|
||||
pub(crate) fn with_date<F: FnMut(&Date)>(&self, mut f: F) {
|
||||
f(&self.current.get().0);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DateService {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("DateService").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DateService {
|
||||
fn drop(&mut self) {
|
||||
// stop the timer update async task on drop.
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
@ -352,7 +352,7 @@ impl ContentEncoder {
|
||||
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding br encoding: {}", err);
|
||||
log::trace!("Error decoding br encoding: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
@ -361,7 +361,7 @@ impl ContentEncoder {
|
||||
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding gzip encoding: {}", err);
|
||||
log::trace!("Error decoding gzip encoding: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
@ -370,7 +370,7 @@ impl ContentEncoder {
|
||||
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding deflate encoding: {}", err);
|
||||
log::trace!("Error decoding deflate encoding: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
@ -379,7 +379,7 @@ impl ContentEncoder {
|
||||
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding ztsd encoding: {}", err);
|
||||
log::trace!("Error decoding ztsd encoding: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
@ -391,9 +391,9 @@ impl ContentEncoder {
|
||||
fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
|
||||
Box::new(brotli::CompressorWriter::new(
|
||||
Writer::new(),
|
||||
8 * 1024, // 32 KiB buffer
|
||||
3, // BROTLI_PARAM_QUALITY
|
||||
22, // BROTLI_PARAM_LGWIN
|
||||
32 * 1024, // 32 KiB buffer
|
||||
3, // BROTLI_PARAM_QUALITY
|
||||
22, // BROTLI_PARAM_LGWIN
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err
|
||||
use derive_more::{Display, Error, From};
|
||||
use http::{uri::InvalidUri, StatusCode};
|
||||
|
||||
use crate::{body::BoxBody, ws, Response};
|
||||
use crate::{body::BoxBody, Response};
|
||||
|
||||
pub use http::Error as HttpError;
|
||||
|
||||
@ -61,6 +61,7 @@ impl Error {
|
||||
Self::new(Kind::Encoder)
|
||||
}
|
||||
|
||||
#[allow(unused)] // used with `ws` feature flag
|
||||
pub(crate) fn new_ws() -> Self {
|
||||
Self::new(Kind::Ws)
|
||||
}
|
||||
@ -139,14 +140,16 @@ impl From<HttpError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ws::HandshakeError> for Error {
|
||||
fn from(err: ws::HandshakeError) -> Self {
|
||||
#[cfg(feature = "ws")]
|
||||
impl From<crate::ws::HandshakeError> for Error {
|
||||
fn from(err: crate::ws::HandshakeError) -> Self {
|
||||
Self::new_ws().with_cause(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ws::ProtocolError> for Error {
|
||||
fn from(err: ws::ProtocolError) -> Self {
|
||||
#[cfg(feature = "ws")]
|
||||
impl From<crate::ws::ProtocolError> for Error {
|
||||
fn from(err: crate::ws::ProtocolError) -> Self {
|
||||
Self::new_ws().with_cause(err)
|
||||
}
|
||||
}
|
||||
@ -277,8 +280,9 @@ pub enum PayloadError {
|
||||
UnknownLength,
|
||||
|
||||
/// HTTP/2 payload error.
|
||||
#[cfg(feature = "http2")]
|
||||
#[display(fmt = "{}", _0)]
|
||||
Http2Payload(h2::Error),
|
||||
Http2Payload(::h2::Error),
|
||||
|
||||
/// Generic I/O error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
@ -293,14 +297,16 @@ impl std::error::Error for PayloadError {
|
||||
PayloadError::EncodingCorrupted => None,
|
||||
PayloadError::Overflow => None,
|
||||
PayloadError::UnknownLength => None,
|
||||
#[cfg(feature = "http2")]
|
||||
PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error),
|
||||
PayloadError::Io(err) => Some(err as &dyn std::error::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<h2::Error> for PayloadError {
|
||||
fn from(err: h2::Error) -> Self {
|
||||
#[cfg(feature = "http2")]
|
||||
impl From<::h2::Error> for PayloadError {
|
||||
fn from(err: ::h2::Error) -> Self {
|
||||
PayloadError::Http2Payload(err)
|
||||
}
|
||||
}
|
||||
@ -356,6 +362,7 @@ pub enum DispatchError {
|
||||
|
||||
/// HTTP/2 error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
#[cfg(feature = "http2")]
|
||||
H2(h2::Error),
|
||||
|
||||
/// The first request did not complete within the specified timeout.
|
||||
@ -379,7 +386,10 @@ impl StdError for DispatchError {
|
||||
DispatchError::Body(err) => Some(&**err),
|
||||
DispatchError::Io(err) => Some(err),
|
||||
DispatchError::Parse(err) => Some(err),
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
DispatchError::H2(err) => Some(err),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::io;
|
||||
use std::{fmt, io};
|
||||
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
use bitflags::bitflags;
|
||||
@ -17,9 +17,9 @@ use crate::{
|
||||
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
const HEAD = 0b0000_0001;
|
||||
const KEEPALIVE_ENABLED = 0b0000_1000;
|
||||
const STREAM = 0b0001_0000;
|
||||
const HEAD = 0b0000_0001;
|
||||
const KEEP_ALIVE_ENABLED = 0b0000_1000;
|
||||
const STREAM = 0b0001_0000;
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ struct ClientCodecInner {
|
||||
decoder: decoder::MessageDecoder<ResponseHead>,
|
||||
payload: Option<PayloadDecoder>,
|
||||
version: Version,
|
||||
ctype: ConnectionType,
|
||||
conn_type: ConnectionType,
|
||||
|
||||
// encoder part
|
||||
flags: Flags,
|
||||
@ -51,23 +51,32 @@ impl Default for ClientCodec {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ClientCodec {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("h1::ClientCodec")
|
||||
.field("flags", &self.inner.flags)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Create HTTP/1 codec.
|
||||
///
|
||||
/// `keepalive_enabled` how response `connection` header get generated.
|
||||
pub fn new(config: ServiceConfig) -> Self {
|
||||
let flags = if config.keep_alive_enabled() {
|
||||
Flags::KEEPALIVE_ENABLED
|
||||
let flags = if config.keep_alive().enabled() {
|
||||
Flags::KEEP_ALIVE_ENABLED
|
||||
} else {
|
||||
Flags::empty()
|
||||
};
|
||||
|
||||
ClientCodec {
|
||||
inner: ClientCodecInner {
|
||||
config,
|
||||
decoder: decoder::MessageDecoder::default(),
|
||||
payload: None,
|
||||
version: Version::HTTP_11,
|
||||
ctype: ConnectionType::Close,
|
||||
conn_type: ConnectionType::Close,
|
||||
|
||||
flags,
|
||||
encoder: encoder::MessageEncoder::default(),
|
||||
@ -77,12 +86,12 @@ impl ClientCodec {
|
||||
|
||||
/// Check if request is upgrade
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.inner.ctype == ConnectionType::Upgrade
|
||||
self.inner.conn_type == ConnectionType::Upgrade
|
||||
}
|
||||
|
||||
/// Check if last response is keep-alive
|
||||
pub fn keepalive(&self) -> bool {
|
||||
self.inner.ctype == ConnectionType::KeepAlive
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.inner.conn_type == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Check last request's message type
|
||||
@ -104,8 +113,8 @@ impl ClientCodec {
|
||||
|
||||
impl ClientPayloadCodec {
|
||||
/// Check if last response is keep-alive
|
||||
pub fn keepalive(&self) -> bool {
|
||||
self.inner.ctype == ConnectionType::KeepAlive
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.inner.conn_type == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Transform payload codec to a message codec
|
||||
@ -122,12 +131,12 @@ impl Decoder for ClientCodec {
|
||||
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
|
||||
|
||||
if let Some((req, payload)) = self.inner.decoder.decode(src)? {
|
||||
if let Some(ctype) = req.conn_type() {
|
||||
if let Some(conn_type) = req.conn_type() {
|
||||
// do not use peer's keep-alive
|
||||
self.inner.ctype = if ctype == ConnectionType::KeepAlive {
|
||||
self.inner.ctype
|
||||
self.inner.conn_type = if conn_type == ConnectionType::KeepAlive {
|
||||
self.inner.conn_type
|
||||
} else {
|
||||
ctype
|
||||
conn_type
|
||||
};
|
||||
}
|
||||
|
||||
@ -192,9 +201,9 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
||||
.set(Flags::HEAD, head.as_ref().method == Method::HEAD);
|
||||
|
||||
// connection status
|
||||
inner.ctype = match head.as_ref().connection_type() {
|
||||
inner.conn_type = match head.as_ref().connection_type() {
|
||||
ConnectionType::KeepAlive => {
|
||||
if inner.flags.contains(Flags::KEEPALIVE_ENABLED) {
|
||||
if inner.flags.contains(Flags::KEEP_ALIVE_ENABLED) {
|
||||
ConnectionType::KeepAlive
|
||||
} else {
|
||||
ConnectionType::Close
|
||||
@ -211,7 +220,7 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
||||
false,
|
||||
inner.version,
|
||||
length,
|
||||
inner.ctype,
|
||||
inner.conn_type,
|
||||
&inner.config,
|
||||
)?;
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ use crate::{
|
||||
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
const HEAD = 0b0000_0001;
|
||||
const KEEPALIVE_ENABLED = 0b0000_0010;
|
||||
const STREAM = 0b0000_0100;
|
||||
const HEAD = 0b0000_0001;
|
||||
const KEEP_ALIVE_ENABLED = 0b0000_0010;
|
||||
const STREAM = 0b0000_0100;
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,9 @@ impl Default for Codec {
|
||||
|
||||
impl fmt::Debug for Codec {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "h1::Codec({:?})", self.flags)
|
||||
f.debug_struct("h1::Codec")
|
||||
.field("flags", &self.flags)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,8 +53,8 @@ impl Codec {
|
||||
///
|
||||
/// `keepalive_enabled` how response `connection` header get generated.
|
||||
pub fn new(config: ServiceConfig) -> Self {
|
||||
let flags = if config.keep_alive_enabled() {
|
||||
Flags::KEEPALIVE_ENABLED
|
||||
let flags = if config.keep_alive().enabled() {
|
||||
Flags::KEEP_ALIVE_ENABLED
|
||||
} else {
|
||||
Flags::empty()
|
||||
};
|
||||
@ -76,14 +78,14 @@ impl Codec {
|
||||
|
||||
/// Check if last response is keep-alive.
|
||||
#[inline]
|
||||
pub fn keepalive(&self) -> bool {
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.conn_type == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Check if keep-alive enabled on server level.
|
||||
#[inline]
|
||||
pub fn keepalive_enabled(&self) -> bool {
|
||||
self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
||||
pub fn keep_alive_enabled(&self) -> bool {
|
||||
self.flags.contains(Flags::KEEP_ALIVE_ENABLED)
|
||||
}
|
||||
|
||||
/// Check last request's message type.
|
||||
@ -124,7 +126,7 @@ impl Decoder for Codec {
|
||||
self.version = head.version;
|
||||
self.conn_type = head.connection_type();
|
||||
if self.conn_type == ConnectionType::KeepAlive
|
||||
&& !self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
||||
&& !self.flags.contains(Flags::KEEP_ALIVE_ENABLED)
|
||||
{
|
||||
self.conn_type = ConnectionType::Close
|
||||
}
|
||||
@ -179,9 +181,11 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
||||
&self.config,
|
||||
)?;
|
||||
}
|
||||
|
||||
Message::Chunk(Some(bytes)) => {
|
||||
self.encoder.encode_chunk(bytes.as_ref(), dst)?;
|
||||
}
|
||||
|
||||
Message::Chunk(None) => {
|
||||
self.encoder.encode_eof(dst)?;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
773
actix-http/src/h1/dispatcher_tests.rs
Normal file
773
actix-http/src/h1/dispatcher_tests.rs
Normal file
@ -0,0 +1,773 @@
|
||||
use std::{future::Future, str, task::Poll, time::Duration};
|
||||
|
||||
use actix_rt::time::sleep;
|
||||
use actix_service::fn_service;
|
||||
use actix_utils::future::{ready, Ready};
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::lazy;
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_service::Service;
|
||||
use bytes::{Buf, BytesMut};
|
||||
|
||||
use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags};
|
||||
use crate::{
|
||||
body::MessageBody,
|
||||
config::ServiceConfig,
|
||||
h1::{Codec, ExpectHandler, UpgradeHandler},
|
||||
service::HttpFlow,
|
||||
test::{TestBuffer, TestSeqBuffer},
|
||||
Error, HttpMessage, KeepAlive, Method, OnConnectData, Request, Response, StatusCode,
|
||||
};
|
||||
|
||||
fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
|
||||
memchr::memmem::find(&haystack[from..], needle)
|
||||
}
|
||||
|
||||
fn stabilize_date_header(payload: &mut [u8]) {
|
||||
let mut from = 0;
|
||||
while let Some(pos) = find_slice(payload, b"date", from) {
|
||||
payload[(from + pos)..(from + pos + 35)]
|
||||
.copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC");
|
||||
from += 35;
|
||||
}
|
||||
}
|
||||
|
||||
fn ok_service() -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error> {
|
||||
status_service(StatusCode::OK)
|
||||
}
|
||||
|
||||
fn status_service(
|
||||
status: StatusCode,
|
||||
) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error> {
|
||||
fn_service(move |_req: Request| ready(Ok::<_, Error>(Response::new(status))))
|
||||
}
|
||||
|
||||
fn echo_path_service(
|
||||
) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error> {
|
||||
fn_service(|req: Request| {
|
||||
let path = req.path().as_bytes();
|
||||
ready(Ok::<_, Error>(
|
||||
Response::ok().set_body(Bytes::copy_from_slice(path)),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
|
||||
fn_service(|mut req: Request| {
|
||||
Box::pin(async move {
|
||||
use futures_util::stream::StreamExt as _;
|
||||
|
||||
let mut pl = req.take_payload();
|
||||
let mut body = BytesMut::new();
|
||||
while let Some(chunk) = pl.next().await {
|
||||
body.extend_from_slice(chunk.unwrap().chunk())
|
||||
}
|
||||
|
||||
Ok::<_, Error>(Response::ok().set_body(body.freeze()))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn late_request() {
|
||||
let mut buf = TestBuffer::empty();
|
||||
|
||||
let cfg = ServiceConfig::new(
|
||||
KeepAlive::Disabled,
|
||||
Duration::from_millis(100),
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let services = HttpFlow::new(ok_service(), ExpectHandler, None);
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
cfg,
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Ready(_) => panic!("first poll should not be ready"),
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
// polls: initial
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
|
||||
buf.extend_read_buf("GET /abcd HTTP/1.1\r\nConnection: close\r\n\r\n");
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Pending => panic!("second poll should not be pending"),
|
||||
Poll::Ready(res) => assert!(res.is_ok()),
|
||||
}
|
||||
|
||||
// polls: initial pending => handle req => shutdown
|
||||
assert_eq!(h1.poll_count, 3);
|
||||
|
||||
let mut res = buf.take_write_buf().to_vec();
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 0\r\n\
|
||||
connection: close\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(exp)
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn oneshot_connection() {
|
||||
let buf = TestBuffer::new("GET /abcd HTTP/1.1\r\n\r\n");
|
||||
|
||||
let cfg = ServiceConfig::new(
|
||||
KeepAlive::Disabled,
|
||||
Duration::from_millis(100),
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
cfg,
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Pending => panic!("first poll should not be pending"),
|
||||
Poll::Ready(res) => assert!(res.is_ok()),
|
||||
}
|
||||
|
||||
// polls: initial => shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
let mut res = buf.take_write_buf().to_vec();
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 5\r\n\
|
||||
connection: close\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||
/abcd\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(exp)
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn keep_alive_timeout() {
|
||||
let buf = TestBuffer::new("GET /abcd HTTP/1.1\r\n\r\n");
|
||||
|
||||
let cfg = ServiceConfig::new(
|
||||
KeepAlive::Timeout(Duration::from_millis(200)),
|
||||
Duration::from_millis(100),
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
cfg,
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
assert!(
|
||||
h1.as_mut().poll(cx).is_pending(),
|
||||
"keep-alive should prevent poll from resolving"
|
||||
);
|
||||
|
||||
// polls: initial
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
|
||||
let mut res = buf.take_write_buf().to_vec();
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 5\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||
/abcd\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(exp)
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
// sleep slightly longer than keep-alive timeout
|
||||
sleep(Duration::from_millis(250)).await;
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(
|
||||
h1.as_mut().poll(cx).is_ready(),
|
||||
"keep-alive should have resolved",
|
||||
);
|
||||
|
||||
// polls: initial => keep-alive wake-up shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||
// connection closed
|
||||
assert!(inner.flags.contains(Flags::SHUTDOWN));
|
||||
assert!(inner.flags.contains(Flags::WRITE_DISCONNECT));
|
||||
// and nothing added to write buffer
|
||||
assert!(buf.write_buf_slice().is_empty());
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn keep_alive_follow_up_req() {
|
||||
let mut buf = TestBuffer::new("GET /abcd HTTP/1.1\r\n\r\n");
|
||||
|
||||
let cfg = ServiceConfig::new(
|
||||
KeepAlive::Timeout(Duration::from_millis(500)),
|
||||
Duration::from_millis(100),
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
cfg,
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
assert!(
|
||||
h1.as_mut().poll(cx).is_pending(),
|
||||
"keep-alive should prevent poll from resolving"
|
||||
);
|
||||
|
||||
// polls: initial
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
|
||||
let mut res = buf.take_write_buf().to_vec();
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 5\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||
/abcd\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(exp)
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
// sleep for less than KA timeout
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(
|
||||
h1.as_mut().poll(cx).is_pending(),
|
||||
"keep-alive should not have resolved dispatcher yet",
|
||||
);
|
||||
|
||||
// polls: initial => manual
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
if let DispatcherStateProj::Normal { inner } = h1.as_mut().project().inner.project() {
|
||||
// connection not closed
|
||||
assert!(!inner.flags.contains(Flags::SHUTDOWN));
|
||||
assert!(!inner.flags.contains(Flags::WRITE_DISCONNECT));
|
||||
// and nothing added to write buffer
|
||||
assert!(buf.write_buf_slice().is_empty());
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
lazy(|cx| {
|
||||
buf.extend_read_buf(
|
||||
"\
|
||||
GET /efg HTTP/1.1\r\n\
|
||||
Connection: close\r\n\
|
||||
\r\n\r\n",
|
||||
);
|
||||
|
||||
assert!(
|
||||
h1.as_mut().poll(cx).is_ready(),
|
||||
"connection close header should override keep-alive setting",
|
||||
);
|
||||
|
||||
// polls: initial => manual => follow-up req => shutdown
|
||||
assert_eq!(h1.poll_count, 4);
|
||||
|
||||
if let DispatcherStateProj::Normal { inner } = h1.as_mut().project().inner.project() {
|
||||
// connection closed
|
||||
assert!(inner.flags.contains(Flags::SHUTDOWN));
|
||||
assert!(!inner.flags.contains(Flags::WRITE_DISCONNECT));
|
||||
}
|
||||
|
||||
let mut res = buf.take_write_buf().to_vec();
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 4\r\n\
|
||||
connection: close\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||
/efg\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(exp)
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn req_parse_err() {
|
||||
lazy(|cx| {
|
||||
let buf = TestBuffer::new("GET /test HTTP/1\r\n\r\n");
|
||||
|
||||
let services = HttpFlow::new(ok_service(), ExpectHandler, None);
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
ServiceConfig::default(),
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Pending => panic!(),
|
||||
Poll::Ready(res) => assert!(res.is_err()),
|
||||
}
|
||||
|
||||
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||
assert!(inner.flags.contains(Flags::READ_DISCONNECT));
|
||||
assert_eq!(
|
||||
&buf.write_buf_slice()[..26],
|
||||
b"HTTP/1.1 400 Bad Request\r\n"
|
||||
);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn pipelining_ok_then_ok() {
|
||||
lazy(|cx| {
|
||||
let buf = TestBuffer::new(
|
||||
"\
|
||||
GET /abcd HTTP/1.1\r\n\r\n\
|
||||
GET /def HTTP/1.1\r\n\r\n\
|
||||
",
|
||||
);
|
||||
|
||||
let cfg = ServiceConfig::new(
|
||||
KeepAlive::Disabled,
|
||||
Duration::from_millis(1),
|
||||
Duration::from_millis(1),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
cfg,
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Pending => panic!("first poll should not be pending"),
|
||||
Poll::Ready(res) => assert!(res.is_ok()),
|
||||
}
|
||||
|
||||
// polls: initial => shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
let mut res = buf.write_buf_slice_mut();
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 5\r\n\
|
||||
connection: close\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||
/abcd\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 4\r\n\
|
||||
connection: close\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||
/def\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(exp)
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn pipelining_ok_then_bad() {
|
||||
lazy(|cx| {
|
||||
let buf = TestBuffer::new(
|
||||
"\
|
||||
GET /abcd HTTP/1.1\r\n\r\n\
|
||||
GET /def HTTP/1\r\n\r\n\
|
||||
",
|
||||
);
|
||||
|
||||
let cfg = ServiceConfig::new(
|
||||
KeepAlive::Disabled,
|
||||
Duration::from_millis(1),
|
||||
Duration::from_millis(1),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
cfg,
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Pending => panic!("first poll should not be pending"),
|
||||
Poll::Ready(res) => assert!(res.is_err()),
|
||||
}
|
||||
|
||||
// polls: initial => shutdown
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
|
||||
let mut res = buf.write_buf_slice_mut();
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = b"\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 5\r\n\
|
||||
connection: close\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||
/abcd\
|
||||
HTTP/1.1 400 Bad Request\r\n\
|
||||
content-length: 0\r\n\
|
||||
connection: close\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(exp)
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn expect_handling() {
|
||||
lazy(|cx| {
|
||||
let mut buf = TestSeqBuffer::empty();
|
||||
let cfg = ServiceConfig::new(
|
||||
KeepAlive::Disabled,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
let services = HttpFlow::new(echo_payload_service(), ExpectHandler, None);
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
cfg,
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
|
||||
buf.extend_read_buf(
|
||||
"\
|
||||
POST /upload HTTP/1.1\r\n\
|
||||
Content-Length: 5\r\n\
|
||||
Expect: 100-continue\r\n\
|
||||
\r\n\
|
||||
",
|
||||
);
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_pending());
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
// polls: manual
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
|
||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||
let io = inner.io.as_ref().unwrap();
|
||||
let res = &io.write_buf()[..];
|
||||
assert_eq!(
|
||||
str::from_utf8(res).unwrap(),
|
||||
"HTTP/1.1 100 Continue\r\n\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
buf.extend_read_buf("12345");
|
||||
assert!(h1.as_mut().poll(cx).is_ready());
|
||||
|
||||
// polls: manual manual shutdown
|
||||
assert_eq!(h1.poll_count, 3);
|
||||
|
||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||
let io = inner.io.as_ref().unwrap();
|
||||
let mut res = (&io.write_buf()[..]).to_owned();
|
||||
stabilize_date_header(&mut res);
|
||||
|
||||
assert_eq!(
|
||||
str::from_utf8(&res).unwrap(),
|
||||
"\
|
||||
HTTP/1.1 100 Continue\r\n\
|
||||
\r\n\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 5\r\n\
|
||||
connection: close\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\
|
||||
\r\n\
|
||||
12345\
|
||||
"
|
||||
);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn expect_eager() {
|
||||
lazy(|cx| {
|
||||
let mut buf = TestSeqBuffer::empty();
|
||||
let cfg = ServiceConfig::new(
|
||||
KeepAlive::Disabled,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
cfg,
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
|
||||
buf.extend_read_buf(
|
||||
"\
|
||||
POST /upload HTTP/1.1\r\n\
|
||||
Content-Length: 5\r\n\
|
||||
Expect: 100-continue\r\n\
|
||||
\r\n\
|
||||
",
|
||||
);
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_ready());
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
// polls: manual shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||
let io = inner.io.as_ref().unwrap();
|
||||
let mut res = (&io.write_buf()[..]).to_owned();
|
||||
stabilize_date_header(&mut res);
|
||||
|
||||
// Despite the content-length header and even though the request payload has not
|
||||
// been sent, this test expects a complete service response since the payload
|
||||
// is not used at all. The service passed to dispatcher is path echo and doesn't
|
||||
// consume payload bytes.
|
||||
assert_eq!(
|
||||
str::from_utf8(&res).unwrap(),
|
||||
"\
|
||||
HTTP/1.1 100 Continue\r\n\
|
||||
\r\n\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
content-length: 7\r\n\
|
||||
connection: close\r\n\
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\
|
||||
\r\n\
|
||||
/upload\
|
||||
"
|
||||
);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn upgrade_handling() {
|
||||
struct TestUpgrade;
|
||||
|
||||
impl<T> Service<(Request, Framed<T, Codec>)> for TestUpgrade {
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&self, (req, _framed): (Request, Framed<T, Codec>)) -> Self::Future {
|
||||
assert_eq!(req.method(), Method::GET);
|
||||
assert!(req.upgrade());
|
||||
assert_eq!(req.headers().get("upgrade").unwrap(), "websocket");
|
||||
ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
lazy(|cx| {
|
||||
let mut buf = TestSeqBuffer::empty();
|
||||
let cfg = ServiceConfig::new(
|
||||
KeepAlive::Disabled,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade));
|
||||
|
||||
let h1 = Dispatcher::<_, _, _, _, TestUpgrade>::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
cfg,
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
|
||||
buf.extend_read_buf(
|
||||
"\
|
||||
GET /ws HTTP/1.1\r\n\
|
||||
Connection: Upgrade\r\n\
|
||||
Upgrade: websocket\r\n\
|
||||
\r\n\
|
||||
",
|
||||
);
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_ready());
|
||||
assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. }));
|
||||
|
||||
// polls: manual shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
})
|
||||
.await;
|
||||
}
|
@ -152,7 +152,6 @@ pub(crate) trait MessageType: Sized {
|
||||
let k = key.as_str().as_bytes();
|
||||
let k_len = k.len();
|
||||
|
||||
// TODO: drain?
|
||||
for val in value.iter() {
|
||||
let v = val.as_ref();
|
||||
let v_len = v.len();
|
||||
@ -213,7 +212,7 @@ pub(crate) trait MessageType: Sized {
|
||||
|
||||
// optimized date header, set_date writes \r\n
|
||||
if !has_date {
|
||||
config.set_date(dst, camel_case);
|
||||
config.write_date_header(dst, camel_case);
|
||||
} else {
|
||||
// msg eof
|
||||
dst.extend_from_slice(b"\r\n");
|
||||
@ -319,16 +318,17 @@ impl MessageType for RequestHeadType {
|
||||
}
|
||||
|
||||
impl<T: MessageType> MessageEncoder<T> {
|
||||
/// Encode message
|
||||
/// Encode chunk.
|
||||
pub fn encode_chunk(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result<bool> {
|
||||
self.te.encode(msg, buf)
|
||||
}
|
||||
|
||||
/// Encode eof
|
||||
/// Encode EOF.
|
||||
pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> {
|
||||
self.te.encode_eof(buf)
|
||||
}
|
||||
|
||||
/// Encode message.
|
||||
pub fn encode(
|
||||
&mut self,
|
||||
dst: &mut BytesMut,
|
||||
|
@ -7,10 +7,13 @@ mod client;
|
||||
mod codec;
|
||||
mod decoder;
|
||||
mod dispatcher;
|
||||
#[cfg(test)]
|
||||
mod dispatcher_tests;
|
||||
mod encoder;
|
||||
mod expect;
|
||||
mod payload;
|
||||
mod service;
|
||||
mod timer;
|
||||
mod upgrade;
|
||||
mod utils;
|
||||
|
||||
@ -26,9 +29,10 @@ pub use self::utils::SendResponse;
|
||||
#[derive(Debug)]
|
||||
/// Codec message
|
||||
pub enum Message<T> {
|
||||
/// Http message
|
||||
/// HTTP message.
|
||||
Item(T),
|
||||
/// Payload chunk
|
||||
|
||||
/// Payload chunk.
|
||||
Chunk(Option<Bytes>),
|
||||
}
|
||||
|
||||
|
80
actix-http/src/h1/timer.rs
Normal file
80
actix-http/src/h1/timer.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use std::{fmt, future::Future, pin::Pin, task::Context};
|
||||
|
||||
use actix_rt::time::{Instant, Sleep};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum TimerState {
|
||||
Disabled,
|
||||
Inactive,
|
||||
Active { timer: Pin<Box<Sleep>> },
|
||||
}
|
||||
|
||||
impl TimerState {
|
||||
pub(super) fn new(enabled: bool) -> Self {
|
||||
if enabled {
|
||||
Self::Inactive
|
||||
} else {
|
||||
Self::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_enabled(&self) -> bool {
|
||||
matches!(self, Self::Active { .. } | Self::Inactive)
|
||||
}
|
||||
|
||||
pub(super) fn set(&mut self, timer: Sleep, line: u32) {
|
||||
if matches!(self, Self::Disabled) {
|
||||
log::trace!("setting disabled timer from line {}", line);
|
||||
}
|
||||
|
||||
*self = Self::Active {
|
||||
timer: Box::pin(timer),
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) fn set_and_init(&mut self, cx: &mut Context<'_>, timer: Sleep, line: u32) {
|
||||
self.set(timer, line);
|
||||
self.init(cx);
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self, line: u32) {
|
||||
if matches!(self, Self::Disabled) {
|
||||
log::trace!("trying to clear a disabled timer from line {}", line);
|
||||
}
|
||||
|
||||
if matches!(self, Self::Inactive) {
|
||||
log::trace!("trying to clear an inactive timer from line {}", line);
|
||||
}
|
||||
|
||||
*self = Self::Inactive;
|
||||
}
|
||||
|
||||
pub(super) fn init(&mut self, cx: &mut Context<'_>) {
|
||||
if let TimerState::Active { timer } = self {
|
||||
let _ = timer.as_mut().poll(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TimerState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TimerState::Disabled => f.write_str("timer is disabled"),
|
||||
TimerState::Inactive => f.write_str("timer is inactive"),
|
||||
TimerState::Active { timer } => {
|
||||
let deadline = timer.deadline();
|
||||
let now = Instant::now();
|
||||
|
||||
if deadline < now {
|
||||
f.write_str("timer is active and has reached deadline")
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"timer is active and due to expire in {} milliseconds",
|
||||
((deadline - now).as_secs_f32() * 1000.0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -57,11 +57,11 @@ where
|
||||
conn_data: OnConnectData,
|
||||
timer: Option<Pin<Box<Sleep>>>,
|
||||
) -> Self {
|
||||
let ping_pong = config.keep_alive().map(|dur| H2PingPong {
|
||||
let ping_pong = config.keep_alive().duration().map(|dur| H2PingPong {
|
||||
timer: timer
|
||||
.map(|mut timer| {
|
||||
// reset timer if it's received from new function.
|
||||
timer.as_mut().reset(config.now() + dur);
|
||||
// reuse timer slot if it was initialized for handshake
|
||||
timer.as_mut().reset((config.now() + dur).into());
|
||||
timer
|
||||
})
|
||||
.unwrap_or_else(|| Box::pin(sleep(dur))),
|
||||
@ -141,7 +141,7 @@ where
|
||||
DispatchError::SendResponse(err) => {
|
||||
trace!("Error sending HTTP/2 response: {:?}", err)
|
||||
}
|
||||
DispatchError::SendData(err) => warn!("{:?}", err),
|
||||
DispatchError::SendData(err) => log::warn!("{:?}", err),
|
||||
DispatchError::ResponseBody(err) => {
|
||||
error!("Response payload stream error: {:?}", err)
|
||||
}
|
||||
@ -160,8 +160,8 @@ where
|
||||
Poll::Ready(_) => {
|
||||
ping_pong.on_flight = false;
|
||||
|
||||
let dead_line = this.config.keep_alive_expire().unwrap();
|
||||
ping_pong.timer.as_mut().reset(dead_line);
|
||||
let dead_line = this.config.keep_alive_deadline().unwrap();
|
||||
ping_pong.timer.as_mut().reset(dead_line.into());
|
||||
}
|
||||
Poll::Pending => {
|
||||
return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(()))
|
||||
@ -174,8 +174,8 @@ where
|
||||
|
||||
ping_pong.ping_pong.send_ping(Ping::opaque())?;
|
||||
|
||||
let dead_line = this.config.keep_alive_expire().unwrap();
|
||||
ping_pong.timer.as_mut().reset(dead_line);
|
||||
let dead_line = this.config.keep_alive_deadline().unwrap();
|
||||
ping_pong.timer.as_mut().reset(dead_line.into());
|
||||
|
||||
ping_pong.on_flight = true;
|
||||
}
|
||||
@ -322,7 +322,7 @@ fn prepare_response(
|
||||
// set date header
|
||||
if !has_date {
|
||||
let mut bytes = BytesMut::with_capacity(29);
|
||||
config.set_date_header(&mut bytes);
|
||||
config.write_date_header_value(&mut bytes);
|
||||
res.headers_mut().insert(
|
||||
DATE,
|
||||
// SAFETY: serialized date-times are known ASCII strings
|
||||
|
@ -7,7 +7,7 @@ use std::{
|
||||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::time::Sleep;
|
||||
use actix_rt::time::{sleep_until, Sleep};
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Stream};
|
||||
use h2::{
|
||||
@ -15,17 +15,17 @@ use h2::{
|
||||
RecvStream,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::ServiceConfig,
|
||||
error::{DispatchError, PayloadError},
|
||||
};
|
||||
|
||||
mod dispatcher;
|
||||
mod service;
|
||||
|
||||
pub use self::dispatcher::Dispatcher;
|
||||
pub use self::service::H2Service;
|
||||
|
||||
use crate::{
|
||||
config::ServiceConfig,
|
||||
error::{DispatchError, PayloadError},
|
||||
};
|
||||
|
||||
/// HTTP/2 peer stream.
|
||||
pub struct Payload {
|
||||
stream: RecvStream,
|
||||
@ -67,7 +67,9 @@ where
|
||||
{
|
||||
HandshakeWithTimeout {
|
||||
handshake: handshake(io),
|
||||
timer: config.client_timer().map(Box::pin),
|
||||
timer: config
|
||||
.client_request_deadline()
|
||||
.map(|deadline| Box::pin(sleep_until(deadline.into()))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +88,7 @@ where
|
||||
let this = self.get_mut();
|
||||
|
||||
match Pin::new(&mut this.handshake).poll(cx)? {
|
||||
// return the timer on success handshake. It can be re-used for h2 ping-pong.
|
||||
// return the timer on success handshake; its slot can be re-used for h2 ping-pong
|
||||
Poll::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))),
|
||||
Poll::Pending => match this.timer.as_mut() {
|
||||
Some(timer) => {
|
||||
|
@ -355,7 +355,7 @@ where
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
trace!("H2 handshake error: {}", err);
|
||||
log::trace!("H2 handshake error: {}", err);
|
||||
Poll::Ready(Err(err))
|
||||
}
|
||||
},
|
||||
|
@ -630,7 +630,7 @@ impl Removed {
|
||||
/// Returns true if iterator contains no elements, without consuming it.
|
||||
///
|
||||
/// If called immediately after [`HeaderMap::insert`] or [`HeaderMap::remove`], it will indicate
|
||||
/// wether any items were actually replaced or removed, respectively.
|
||||
/// whether any items were actually replaced or removed, respectively.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self.inner {
|
||||
// size hint lower bound of smallvec is the correct length
|
||||
|
@ -4,8 +4,7 @@ use bytes::BytesMut;
|
||||
use http::header::{HeaderValue, InvalidHeaderValue};
|
||||
|
||||
use crate::{
|
||||
config::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue,
|
||||
helpers::MutWriter,
|
||||
date::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue, helpers::MutWriter,
|
||||
};
|
||||
|
||||
/// A timestamp with HTTP-style formatting and parsing.
|
||||
|
@ -55,7 +55,7 @@ pub trait HttpMessage: Sized {
|
||||
""
|
||||
}
|
||||
|
||||
/// Get content type encoding
|
||||
/// Get content type encoding.
|
||||
///
|
||||
/// UTF-8 is used by default, If request charset is not set.
|
||||
fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> {
|
||||
|
84
actix-http/src/keep_alive.rs
Normal file
84
actix-http/src/keep_alive.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use std::time::Duration;
|
||||
|
||||
/// Connection keep-alive config.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum KeepAlive {
|
||||
/// Keep-alive duration.
|
||||
///
|
||||
/// `KeepAlive::Timeout(Duration::ZERO)` is mapped to `KeepAlive::Disabled`.
|
||||
Timeout(Duration),
|
||||
|
||||
/// Rely on OS to shutdown TCP connection.
|
||||
///
|
||||
/// Some defaults can be very long, check your OS documentation.
|
||||
Os,
|
||||
|
||||
/// Keep-alive is disabled.
|
||||
///
|
||||
/// Connections will be closed immediately.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl KeepAlive {
|
||||
pub(crate) fn enabled(&self) -> bool {
|
||||
!matches!(self, Self::Disabled)
|
||||
}
|
||||
|
||||
#[allow(unused)] // used with `http2` feature flag
|
||||
pub(crate) fn duration(&self) -> Option<Duration> {
|
||||
match self {
|
||||
KeepAlive::Timeout(dur) => Some(*dur),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map zero duration to disabled.
|
||||
pub(crate) fn normalize(self) -> KeepAlive {
|
||||
match self {
|
||||
KeepAlive::Timeout(Duration::ZERO) => KeepAlive::Disabled,
|
||||
ka => ka,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KeepAlive {
|
||||
fn default() -> Self {
|
||||
Self::Timeout(Duration::from_secs(5))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Duration> for KeepAlive {
|
||||
fn from(dur: Duration) -> Self {
|
||||
KeepAlive::Timeout(dur).normalize()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Duration>> for KeepAlive {
|
||||
fn from(ka_dur: Option<Duration>) -> Self {
|
||||
match ka_dur {
|
||||
Some(dur) => KeepAlive::from(dur),
|
||||
None => KeepAlive::Disabled,
|
||||
}
|
||||
.normalize()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_impls() {
|
||||
let test: KeepAlive = Duration::from_secs(1).into();
|
||||
assert_eq!(test, KeepAlive::Timeout(Duration::from_secs(1)));
|
||||
|
||||
let test: KeepAlive = Duration::from_secs(0).into();
|
||||
assert_eq!(test, KeepAlive::Disabled);
|
||||
|
||||
let test: KeepAlive = Some(Duration::from_secs(0)).into();
|
||||
assert_eq!(test, KeepAlive::Disabled);
|
||||
|
||||
let test: KeepAlive = None.into();
|
||||
assert_eq!(test, KeepAlive::Disabled);
|
||||
}
|
||||
}
|
@ -24,38 +24,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;
|
||||
|
||||
pub use ::http::{uri, uri::Uri};
|
||||
pub use ::http::{Method, StatusCode, Version};
|
||||
|
||||
pub mod body;
|
||||
mod builder;
|
||||
mod config;
|
||||
mod date;
|
||||
#[cfg(feature = "__compress")]
|
||||
pub mod encoding;
|
||||
pub mod error;
|
||||
mod extensions;
|
||||
pub mod h1;
|
||||
#[cfg(feature = "http2")]
|
||||
pub mod h2;
|
||||
pub mod header;
|
||||
mod helpers;
|
||||
mod http_message;
|
||||
mod keep_alive;
|
||||
mod message;
|
||||
#[cfg(test)]
|
||||
mod notify_on_drop;
|
||||
mod payload;
|
||||
mod requests;
|
||||
mod responses;
|
||||
mod service;
|
||||
pub mod test;
|
||||
#[cfg(feature = "ws")]
|
||||
pub mod ws;
|
||||
|
||||
pub use self::builder::HttpServiceBuilder;
|
||||
pub use self::config::{KeepAlive, ServiceConfig};
|
||||
pub use self::config::ServiceConfig;
|
||||
pub use self::error::Error;
|
||||
pub use self::extensions::Extensions;
|
||||
pub use self::header::ContentEncoding;
|
||||
pub use self::http_message::HttpMessage;
|
||||
pub use self::keep_alive::KeepAlive;
|
||||
pub use self::message::ConnectionType;
|
||||
pub use self::message::Message;
|
||||
#[allow(deprecated)]
|
||||
|
49
actix-http/src/notify_on_drop.rs
Normal file
49
actix-http/src/notify_on_drop.rs
Normal file
@ -0,0 +1,49 @@
|
||||
/// Test Module for checking the drop state of certain async tasks that are spawned
|
||||
/// with `actix_rt::spawn`
|
||||
///
|
||||
/// The target task must explicitly generate `NotifyOnDrop` when spawn the task
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
/// Check if the spawned task is dropped.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics when there was no `NotifyOnDrop` instance on current thread.
|
||||
pub(crate) fn is_dropped() -> bool {
|
||||
NOTIFY_DROPPED.with(|bool| {
|
||||
bool.borrow()
|
||||
.expect("No NotifyOnDrop existed on current thread")
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) struct NotifyOnDrop;
|
||||
|
||||
impl NotifyOnDrop {
|
||||
/// # Panics
|
||||
/// Panics hen construct multiple instances on any given thread.
|
||||
pub(crate) fn new() -> Self {
|
||||
NOTIFY_DROPPED.with(|bool| {
|
||||
let mut bool = bool.borrow_mut();
|
||||
if bool.is_some() {
|
||||
panic!("NotifyOnDrop existed on current thread");
|
||||
} else {
|
||||
*bool = Some(false);
|
||||
}
|
||||
});
|
||||
|
||||
NotifyOnDrop
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NotifyOnDrop {
|
||||
fn drop(&mut self) {
|
||||
NOTIFY_DROPPED.with(|bool| {
|
||||
if let Some(b) = bool.borrow_mut().as_mut() {
|
||||
*b = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ use std::{
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::error::PayloadError;
|
||||
|
||||
@ -15,7 +16,19 @@ pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadErr
|
||||
#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
||||
pub type PayloadStream = BoxedPayloadStream;
|
||||
|
||||
pin_project_lite::pin_project! {
|
||||
#[cfg(not(feature = "http2"))]
|
||||
pin_project! {
|
||||
/// A streaming payload.
|
||||
#[project = PayloadProj]
|
||||
pub enum Payload<S = BoxedPayloadStream> {
|
||||
None,
|
||||
H1 { payload: crate::h1::Payload },
|
||||
Stream { #[pin] payload: S },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
pin_project! {
|
||||
/// A streaming payload.
|
||||
#[project = PayloadProj]
|
||||
pub enum Payload<S = BoxedPayloadStream> {
|
||||
@ -32,14 +45,16 @@ impl<S> From<crate::h1::Payload> for Payload<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
impl<S> From<crate::h2::Payload> for Payload<S> {
|
||||
fn from(payload: crate::h2::Payload) -> Self {
|
||||
Payload::H2 { payload }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<h2::RecvStream> for Payload<S> {
|
||||
fn from(stream: h2::RecvStream) -> Self {
|
||||
#[cfg(feature = "http2")]
|
||||
impl<S> From<::h2::RecvStream> for Payload<S> {
|
||||
fn from(stream: ::h2::RecvStream) -> Self {
|
||||
Payload::H2 {
|
||||
payload: crate::h2::Payload::new(stream),
|
||||
}
|
||||
@ -70,7 +85,10 @@ where
|
||||
match self.project() {
|
||||
PayloadProj::None => Poll::Ready(None),
|
||||
PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx),
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx),
|
||||
|
||||
PayloadProj::Stream { payload } => payload.poll_next(cx),
|
||||
}
|
||||
}
|
||||
|
@ -130,8 +130,8 @@ impl RequestHead {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request contains `EXPECT` header.
|
||||
#[inline]
|
||||
/// Request contains `EXPECT` header
|
||||
pub fn expect(&self) -> bool {
|
||||
self.flags.contains(Flags::EXPECT)
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ impl ResponseHead {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Sets the flag that controls wether to send headers formatted as Camel-Case.
|
||||
/// Sets the flag that controls whether to send headers formatted as Camel-Case.
|
||||
///
|
||||
/// Only applicable to HTTP/1.x responses; HTTP/2 header names are always lowercase.
|
||||
#[inline]
|
||||
@ -210,14 +210,15 @@ mod tests {
|
||||
use memchr::memmem;
|
||||
|
||||
use crate::{
|
||||
h1::H1Service,
|
||||
header::{HeaderName, HeaderValue},
|
||||
Error, HttpService, Request, Response,
|
||||
Error, Request, Response, ServiceConfig,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn camel_case_headers() {
|
||||
let mut srv = actix_http_test::test_server(|| {
|
||||
HttpService::new(|req: Request| async move {
|
||||
H1Service::with_config(ServiceConfig::default(), |req: Request| async move {
|
||||
let mut res = Response::ok();
|
||||
|
||||
if req.path().contains("camel") {
|
||||
@ -228,6 +229,7 @@ mod tests {
|
||||
HeaderName::from_static("foo-bar"),
|
||||
HeaderValue::from_static("baz"),
|
||||
);
|
||||
|
||||
Ok::<_, Error>(res)
|
||||
})
|
||||
.tcp()
|
||||
@ -235,9 +237,11 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
let _ = stream.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n");
|
||||
let mut data = vec![0; 1024];
|
||||
let _ = stream.read(&mut data);
|
||||
let _ = stream
|
||||
.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
||||
.unwrap();
|
||||
let mut data = vec![];
|
||||
let _ = stream.read_to_end(&mut data).unwrap();
|
||||
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
|
||||
assert!(memmem::find(&data, b"Foo-Bar").is_some());
|
||||
assert!(memmem::find(&data, b"foo-bar").is_none());
|
||||
@ -247,9 +251,11 @@ mod tests {
|
||||
assert!(memmem::find(&data, b"content-length").is_none());
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
let _ = stream.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n");
|
||||
let mut data = vec![0; 1024];
|
||||
let _ = stream.read(&mut data);
|
||||
let _ = stream
|
||||
.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
||||
.unwrap();
|
||||
let mut data = vec![];
|
||||
let _ = stream.read_to_end(&mut data).unwrap();
|
||||
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
|
||||
assert!(memmem::find(&data, b"Foo-Bar").is_none());
|
||||
assert!(memmem::find(&data, b"foo-bar").is_some());
|
||||
|
@ -185,7 +185,7 @@ impl<B> Response<B> {
|
||||
self.replace_body(())
|
||||
}
|
||||
|
||||
/// Map the current body type to another using a closure. Returns a new response.
|
||||
/// Map the current body type to another using a closure, returning a new response.
|
||||
///
|
||||
/// Closure receives the response head and the current body type.
|
||||
#[inline]
|
||||
@ -202,6 +202,7 @@ impl<B> Response<B> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the current body to a type-erased `BoxBody`.
|
||||
#[inline]
|
||||
pub fn map_into_boxed_body(self) -> Response<BoxBody>
|
||||
where
|
||||
@ -210,7 +211,7 @@ impl<B> Response<B> {
|
||||
self.map_body(|_, body| body.boxed())
|
||||
}
|
||||
|
||||
/// Returns body, consuming this response.
|
||||
/// Returns the response body, dropping all other parts.
|
||||
#[inline]
|
||||
pub fn into_body(self) -> B {
|
||||
self.body
|
||||
|
@ -19,9 +19,8 @@ use pin_project_lite::pin_project;
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
builder::HttpServiceBuilder,
|
||||
config::{KeepAlive, ServiceConfig},
|
||||
error::DispatchError,
|
||||
h1, h2, ConnectCallback, OnConnectData, Protocol, Request, Response,
|
||||
h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig,
|
||||
};
|
||||
|
||||
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
|
||||
@ -43,9 +42,9 @@ where
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create builder for `HttpService` instance.
|
||||
/// Constructs builder for `HttpService` instance.
|
||||
pub fn build() -> HttpServiceBuilder<T, S> {
|
||||
HttpServiceBuilder::new()
|
||||
HttpServiceBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,12 +57,10 @@ where
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create new `HttpService` instance.
|
||||
/// Constructs new `HttpService` instance from service with default config.
|
||||
pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self {
|
||||
let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0, false, None);
|
||||
|
||||
HttpService {
|
||||
cfg,
|
||||
cfg: ServiceConfig::default(),
|
||||
srv: service.into_factory(),
|
||||
expect: h1::ExpectHandler,
|
||||
upgrade: None,
|
||||
@ -72,7 +69,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new `HttpService` instance with config.
|
||||
/// Constructs new `HttpService` instance from config and service.
|
||||
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
|
||||
cfg: ServiceConfig,
|
||||
service: F,
|
||||
@ -97,11 +94,10 @@ where
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody,
|
||||
{
|
||||
/// Provide service for `EXPECT: 100-Continue` support.
|
||||
/// Sets service for `Expect: 100-Continue` handling.
|
||||
///
|
||||
/// Service get called with request that contains `EXPECT` header.
|
||||
/// Service must return request in case of success, in that case
|
||||
/// request will be forwarded to main service.
|
||||
/// An expect service is called with requests that contain an `Expect` header. A successful
|
||||
/// response type is also a request which will be forwarded to the main service.
|
||||
pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U>
|
||||
where
|
||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
@ -118,10 +114,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide service for custom `Connection: UPGRADE` support.
|
||||
/// Sets service for custom `Connection: Upgrade` handling.
|
||||
///
|
||||
/// If service is provided then normal requests handling get halted
|
||||
/// and this service get called with original request and framed object.
|
||||
/// If service is provided then normal requests handling get halted and this service get called
|
||||
/// with original request and framed object.
|
||||
pub fn upgrade<U1>(self, upgrade: Option<U1>) -> HttpService<T, S, B, X, U1>
|
||||
where
|
||||
U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||
@ -506,10 +502,11 @@ where
|
||||
let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||
|
||||
match proto {
|
||||
#[cfg(feature = "http2")]
|
||||
Protocol::Http2 => HttpServiceHandlerResponse {
|
||||
state: State::H2Handshake {
|
||||
handshake: Some((
|
||||
h2::handshake_with_timeout(io, &self.cfg),
|
||||
crate::h2::handshake_with_timeout(io, &self.cfg),
|
||||
self.cfg.clone(),
|
||||
self.flow.clone(),
|
||||
conn_data,
|
||||
@ -518,6 +515,11 @@ where
|
||||
},
|
||||
},
|
||||
|
||||
#[cfg(not(feature = "http2"))]
|
||||
Protocol::Http2 => {
|
||||
panic!("HTTP/2 support is disabled (enable with the `http2` feature flag)")
|
||||
}
|
||||
|
||||
Protocol::Http1 => HttpServiceHandlerResponse {
|
||||
state: State::H1 {
|
||||
dispatcher: h1::Dispatcher::new(
|
||||
@ -535,6 +537,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "http2"))]
|
||||
pin_project! {
|
||||
#[project = StateProj]
|
||||
enum State<T, S, B, X, U>
|
||||
@ -556,10 +559,37 @@ pin_project! {
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
H1 { #[pin] dispatcher: h1::Dispatcher<T, S, B, X, U> },
|
||||
H2 { #[pin] dispatcher: h2::Dispatcher<T, S, B, X, U> },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
pin_project! {
|
||||
#[project = StateProj]
|
||||
enum State<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead,
|
||||
T: AsyncWrite,
|
||||
T: Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
H1 { #[pin] dispatcher: h1::Dispatcher<T, S, B, X, U> },
|
||||
|
||||
H2 { #[pin] dispatcher: crate::h2::Dispatcher<T, S, B, X, U> },
|
||||
|
||||
H2Handshake {
|
||||
handshake: Option<(
|
||||
h2::HandshakeWithTimeout<T>,
|
||||
crate::h2::HandshakeWithTimeout<T>,
|
||||
ServiceConfig,
|
||||
Rc<HttpFlow<S, X, U>>,
|
||||
OnConnectData,
|
||||
@ -618,21 +648,25 @@ where
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.as_mut().project().state.project() {
|
||||
StateProj::H1 { dispatcher } => dispatcher.poll(cx),
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
StateProj::H2 { dispatcher } => dispatcher.poll(cx),
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
StateProj::H2Handshake { handshake: data } => {
|
||||
match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) {
|
||||
Ok((conn, timer)) => {
|
||||
let (_, config, flow, conn_data, peer_addr) = data.take().unwrap();
|
||||
|
||||
self.as_mut().project().state.set(State::H2 {
|
||||
dispatcher: h2::Dispatcher::new(
|
||||
dispatcher: crate::h2::Dispatcher::new(
|
||||
conn, flow, config, peer_addr, conn_data, timer,
|
||||
),
|
||||
});
|
||||
self.poll(cx)
|
||||
}
|
||||
Err(err) => {
|
||||
trace!("H2 handshake error: {}", err);
|
||||
log::trace!("H2 handshake error: {}", err);
|
||||
Poll::Ready(Err(err))
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Various testing helpers for use in internal and app tests.
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefCell},
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
io::{self, Read, Write},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
@ -157,10 +157,11 @@ fn parts(parts: &mut Option<Inner>) -> &mut Inner {
|
||||
}
|
||||
|
||||
/// Async I/O test buffer.
|
||||
#[derive(Debug)]
|
||||
pub struct TestBuffer {
|
||||
pub read_buf: BytesMut,
|
||||
pub write_buf: BytesMut,
|
||||
pub err: Option<io::Error>,
|
||||
pub read_buf: Rc<RefCell<BytesMut>>,
|
||||
pub write_buf: Rc<RefCell<BytesMut>>,
|
||||
pub err: Option<Rc<io::Error>>,
|
||||
}
|
||||
|
||||
impl TestBuffer {
|
||||
@ -170,34 +171,69 @@ impl TestBuffer {
|
||||
T: Into<BytesMut>,
|
||||
{
|
||||
Self {
|
||||
read_buf: data.into(),
|
||||
write_buf: BytesMut::new(),
|
||||
read_buf: Rc::new(RefCell::new(data.into())),
|
||||
write_buf: Rc::new(RefCell::new(BytesMut::new())),
|
||||
err: None,
|
||||
}
|
||||
}
|
||||
|
||||
// intentionally not using Clone trait
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn clone(&self) -> Self {
|
||||
Self {
|
||||
read_buf: self.read_buf.clone(),
|
||||
write_buf: self.write_buf.clone(),
|
||||
err: self.err.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new empty `TestBuffer` instance.
|
||||
pub fn empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn read_buf_slice(&self) -> Ref<'_, [u8]> {
|
||||
Ref::map(self.read_buf.borrow(), |b| b.as_ref())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn read_buf_slice_mut(&self) -> RefMut<'_, [u8]> {
|
||||
RefMut::map(self.read_buf.borrow_mut(), |b| b.as_mut())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn write_buf_slice(&self) -> Ref<'_, [u8]> {
|
||||
Ref::map(self.write_buf.borrow(), |b| b.as_ref())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn write_buf_slice_mut(&self) -> RefMut<'_, [u8]> {
|
||||
RefMut::map(self.write_buf.borrow_mut(), |b| b.as_mut())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn take_write_buf(&self) -> Bytes {
|
||||
self.write_buf.borrow_mut().split().freeze()
|
||||
}
|
||||
|
||||
/// Add data to read buffer.
|
||||
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
|
||||
self.read_buf.extend_from_slice(data.as_ref())
|
||||
self.read_buf.borrow_mut().extend_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Read for TestBuffer {
|
||||
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
|
||||
if self.read_buf.is_empty() {
|
||||
if self.read_buf.borrow().is_empty() {
|
||||
if self.err.is_some() {
|
||||
Err(self.err.take().unwrap())
|
||||
Err(Rc::try_unwrap(self.err.take().unwrap()).unwrap())
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
|
||||
}
|
||||
} else {
|
||||
let size = std::cmp::min(self.read_buf.len(), dst.len());
|
||||
let b = self.read_buf.split_to(size);
|
||||
let size = std::cmp::min(self.read_buf.borrow().len(), dst.len());
|
||||
let b = self.read_buf.borrow_mut().split_to(size);
|
||||
dst[..size].copy_from_slice(&b);
|
||||
Ok(size)
|
||||
}
|
||||
@ -206,7 +242,7 @@ impl io::Read for TestBuffer {
|
||||
|
||||
impl io::Write for TestBuffer {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.write_buf.extend(buf);
|
||||
self.write_buf.borrow_mut().extend(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,11 @@ use bitflags::bitflags;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
|
||||
use super::frame::Parser;
|
||||
use super::proto::{CloseReason, OpCode};
|
||||
use super::ProtocolError;
|
||||
use super::{
|
||||
frame::Parser,
|
||||
proto::{CloseReason, OpCode},
|
||||
ProtocolError,
|
||||
};
|
||||
|
||||
/// A WebSocket message.
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -251,7 +253,7 @@ impl Decoder for Codec {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!("Unfinished fragment {:?}", opcode);
|
||||
log::error!("Unfinished fragment {:?}", opcode);
|
||||
Err(ProtocolError::ContinuationFragment(opcode))
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_service::{IntoService, Service};
|
||||
|
@ -3,9 +3,11 @@ use std::convert::TryFrom;
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use log::debug;
|
||||
|
||||
use crate::ws::mask::apply_mask;
|
||||
use crate::ws::proto::{CloseCode, CloseReason, OpCode};
|
||||
use crate::ws::ProtocolError;
|
||||
use super::{
|
||||
mask::apply_mask,
|
||||
proto::{CloseCode, CloseReason, OpCode},
|
||||
ProtocolError,
|
||||
};
|
||||
|
||||
/// A struct representing a WebSocket frame.
|
||||
#[derive(Debug)]
|
||||
|
@ -31,7 +31,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World";
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_v2() {
|
||||
async fn h1_v2() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -59,7 +59,7 @@ async fn test_h1_v2() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_connection_close() {
|
||||
async fn connection_close() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -73,7 +73,7 @@ async fn test_connection_close() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_with_query_parameter() {
|
||||
async fn with_query_parameter() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|req: Request| async move {
|
||||
@ -104,7 +104,7 @@ impl From<ExpectFailed> for Response<BoxBody> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_expect() {
|
||||
async fn h1_expect() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.expect(|req: Request| async {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::io;
|
||||
use std::{io, time::Duration};
|
||||
|
||||
use actix_http::{error::Error, HttpService, Response};
|
||||
use actix_server::Server;
|
||||
@ -19,7 +19,7 @@ async fn h2_ping_pong() -> io::Result<()> {
|
||||
.workers(1)
|
||||
.listen("h2_ping_pong", lst, || {
|
||||
HttpService::build()
|
||||
.keep_alive(3)
|
||||
.keep_alive(Duration::from_secs(3))
|
||||
.h2(|_| async { Ok::<_, Error>(Response::ok()) })
|
||||
.tcp()
|
||||
})?
|
||||
@ -92,10 +92,10 @@ async fn h2_handshake_timeout() -> io::Result<()> {
|
||||
.workers(1)
|
||||
.listen("h2_ping_pong", lst, || {
|
||||
HttpService::build()
|
||||
.keep_alive(30)
|
||||
.keep_alive(Duration::from_secs(30))
|
||||
// set first request timeout to 5 seconds.
|
||||
// this is the timeout used for http2 handshake.
|
||||
.client_timeout(5000)
|
||||
.client_request_timeout(Duration::from_secs(5))
|
||||
.h2(|_| async { Ok::<_, Error>(Response::ok()) })
|
||||
.tcp()
|
||||
})?
|
||||
|
@ -2,7 +2,7 @@ use std::{
|
||||
convert::Infallible,
|
||||
io::{Read, Write},
|
||||
net, thread,
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use actix_http::{
|
||||
@ -22,12 +22,12 @@ use futures_util::{
|
||||
use regex::Regex;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1() {
|
||||
async fn h1_basic() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
.h1(|req: Request| {
|
||||
assert!(req.peer_addr().is_some());
|
||||
ok::<_, Infallible>(Response::ok())
|
||||
@ -43,12 +43,12 @@ async fn test_h1() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_2() {
|
||||
async fn h1_2() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
.finish(|req: Request| {
|
||||
assert!(req.peer_addr().is_some());
|
||||
assert_eq!(req.version(), http::Version::HTTP_11);
|
||||
@ -75,7 +75,7 @@ impl From<ExpectFailed> for Response<BoxBody> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_expect_continue() {
|
||||
async fn expect_continue() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.expect(fn_service(|req: Request| {
|
||||
@ -106,7 +106,7 @@ async fn test_expect_continue() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_expect_continue_h1() {
|
||||
async fn expect_continue_h1() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.expect(fn_service(|req: Request| {
|
||||
@ -139,7 +139,7 @@ async fn test_expect_continue_h1() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_chunked_payload() {
|
||||
async fn chunked_payload() {
|
||||
let chunk_sizes = vec![32768, 32, 32768];
|
||||
let total_size: usize = chunk_sizes.iter().sum();
|
||||
|
||||
@ -197,26 +197,43 @@ async fn test_chunked_payload() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_slow_request() {
|
||||
async fn slow_request_408() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.client_timeout(100)
|
||||
.client_request_timeout(Duration::from_millis(200))
|
||||
.keep_alive(Duration::from_secs(2))
|
||||
.finish(|_| ok::<_, Infallible>(Response::ok()))
|
||||
.tcp()
|
||||
})
|
||||
.await;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n");
|
||||
let _ = stream.write_all(b"GET /test HTTP/1.1\r\n");
|
||||
let mut data = String::new();
|
||||
let _ = stream.read_to_string(&mut data);
|
||||
assert!(data.starts_with("HTTP/1.1 408 Request Timeout"));
|
||||
assert!(
|
||||
data.starts_with("HTTP/1.1 408 Request Timeout"),
|
||||
"response was not 408: {}",
|
||||
data
|
||||
);
|
||||
|
||||
let diff = start.elapsed();
|
||||
|
||||
if diff < Duration::from_secs(1) {
|
||||
// test success
|
||||
} else if diff < Duration::from_secs(3) {
|
||||
panic!("request seems to have wrongly timed-out according to keep-alive");
|
||||
} else {
|
||||
panic!("request took way too long to time out");
|
||||
}
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_malformed_request() {
|
||||
async fn http1_malformed_request() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -234,7 +251,7 @@ async fn test_http1_malformed_request() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive() {
|
||||
async fn http1_keepalive() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -257,23 +274,25 @@ async fn test_http1_keepalive() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive_timeout() {
|
||||
async fn http1_keepalive_timeout() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(1)
|
||||
.keep_alive(Duration::from_secs(1))
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
.tcp()
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n");
|
||||
let mut data = vec![0; 1024];
|
||||
|
||||
let _ = stream.write_all(b"GET /test HTTP/1.1\r\n\r\n");
|
||||
let mut data = vec![0; 256];
|
||||
let _ = stream.read(&mut data);
|
||||
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
|
||||
|
||||
thread::sleep(Duration::from_millis(1100));
|
||||
|
||||
let mut data = vec![0; 1024];
|
||||
let mut data = vec![0; 256];
|
||||
let res = stream.read(&mut data).unwrap();
|
||||
assert_eq!(res, 0);
|
||||
|
||||
@ -281,7 +300,7 @@ async fn test_http1_keepalive_timeout() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive_close() {
|
||||
async fn http1_keepalive_close() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -303,7 +322,7 @@ async fn test_http1_keepalive_close() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http10_keepalive_default_close() {
|
||||
async fn http10_keepalive_default_close() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -325,7 +344,7 @@ async fn test_http10_keepalive_default_close() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http10_keepalive() {
|
||||
async fn http10_keepalive() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -354,7 +373,7 @@ async fn test_http10_keepalive() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive_disabled() {
|
||||
async fn http1_keepalive_disabled() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
@ -377,7 +396,7 @@ async fn test_http1_keepalive_disabled() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_content_length() {
|
||||
async fn content_length() {
|
||||
use actix_http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
StatusCode,
|
||||
@ -426,7 +445,7 @@ async fn test_content_length() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_headers() {
|
||||
async fn h1_headers() {
|
||||
let data = STR.repeat(10);
|
||||
let data2 = data.clone();
|
||||
|
||||
@ -492,7 +511,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World";
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_body() {
|
||||
async fn h1_body() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -511,7 +530,7 @@ async fn test_h1_body() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_head_empty() {
|
||||
async fn h1_head_empty() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -538,7 +557,7 @@ async fn test_h1_head_empty() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_head_binary() {
|
||||
async fn h1_head_binary() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -565,7 +584,7 @@ async fn test_h1_head_binary() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_head_binary2() {
|
||||
async fn h1_head_binary2() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -588,7 +607,7 @@ async fn test_h1_head_binary2() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_body_length() {
|
||||
async fn h1_body_length() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| {
|
||||
@ -612,7 +631,7 @@ async fn test_h1_body_length() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_body_chunked_explicit() {
|
||||
async fn h1_body_chunked_explicit() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| {
|
||||
@ -649,7 +668,7 @@ async fn test_h1_body_chunked_explicit() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_body_chunked_implicit() {
|
||||
async fn h1_body_chunked_implicit() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| {
|
||||
@ -680,7 +699,7 @@ async fn test_h1_body_chunked_implicit() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_response_http_error_handling() {
|
||||
async fn h1_response_http_error_handling() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(fn_service(|_| {
|
||||
@ -719,7 +738,7 @@ impl From<BadRequest> for Response<BoxBody> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_service_error() {
|
||||
async fn h1_service_error() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| err::<Response<()>, _>(BadRequest))
|
||||
@ -738,7 +757,7 @@ async fn test_h1_service_error() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_on_connect() {
|
||||
async fn h1_on_connect() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.on_connect_ext(|_, data| {
|
||||
@ -761,7 +780,7 @@ async fn test_h1_on_connect() {
|
||||
/// Tests compliance with 304 Not Modified spec in RFC 7232 §4.1.
|
||||
/// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
|
||||
#[actix_rt::test]
|
||||
async fn test_not_modified_spec_h1() {
|
||||
async fn not_modified_spec_h1() {
|
||||
// TODO: this test needing a few seconds to complete reveals some weirdness with either the
|
||||
// dispatcher or the client, though similar hangs occur on other tests in this file, only
|
||||
// succeeding, it seems, because of the keepalive timer
|
||||
|
@ -109,7 +109,7 @@ async fn service(msg: Frame) -> Result<Message, Error> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_simple() {
|
||||
async fn simple() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.upgrade(fn_factory(|| async {
|
||||
|
@ -3,6 +3,10 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.4.0-beta.13 - 2022-01-31
|
||||
- No significant changes since `0.4.0-beta.12`.
|
||||
|
||||
|
||||
## 0.4.0-beta.12 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.4.0-beta.12"
|
||||
version = "0.4.0-beta.13"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Multipart form support for Actix Web"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -15,7 +15,7 @@ path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.21", default-features = false }
|
||||
actix-web = { version = "4.0.0-rc.1", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
@ -28,7 +28,7 @@ twoway = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-http = "3.0.0-beta.19"
|
||||
actix-http = "3.0.0-rc.1"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Multipart form support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://docs.rs/actix-multipart/0.4.0-beta.12)
|
||||
[](https://docs.rs/actix-multipart/0.4.0-beta.13)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.12)
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.13)
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -3,6 +3,18 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.5.0-rc.3 - 2022-01-31
|
||||
- Remove unused `ResourceInfo`. [#2612]
|
||||
- Add `RouterBuilder::push`. [#2612]
|
||||
- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612]
|
||||
- Replace `Option<U>` with `U` in `Router` API. [#2612]
|
||||
- Relax bounds on `Router::recognize*` and `ResourceDef::capture_match_info`. [#2612]
|
||||
- `Quoter::requote` now returns `Option<Vec<u8>>`. [#2613]
|
||||
|
||||
[#2612]: https://github.com/actix/actix-web/pull/2612
|
||||
[#2613]: https://github.com/actix/actix-web/pull/2613
|
||||
|
||||
|
||||
## 0.5.0-rc.2 - 2022-01-21
|
||||
- Add `Path::as_str`. [#2590]
|
||||
- Deprecate `Path::path`. [#2590]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-router"
|
||||
version = "0.5.0-rc.2"
|
||||
version = "0.5.0-rc.3"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
||||
|
@ -52,7 +52,7 @@ macro_rules! parse_value {
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
let decoded = FULL_QUOTER
|
||||
.with(|q| q.requote(self.value.as_bytes()))
|
||||
.with(|q| q.requote_str_lossy(self.value))
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or(Cow::Borrowed(self.value));
|
||||
|
||||
@ -332,7 +332,7 @@ impl<'de> Deserializer<'de> for Value<'de> {
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) {
|
||||
match FULL_QUOTER.with(|q| q.requote_str_lossy(self.value)) {
|
||||
Some(s) => visitor.visit_string(s),
|
||||
None => visitor.visit_borrowed_str(self.value),
|
||||
}
|
||||
@ -342,7 +342,7 @@ impl<'de> Deserializer<'de> for Value<'de> {
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) {
|
||||
match FULL_QUOTER.with(|q| q.requote_str_lossy(self.value)) {
|
||||
Some(s) => visitor.visit_byte_buf(s.into()),
|
||||
None => visitor.visit_borrowed_bytes(self.value.as_bytes()),
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ pub use self::pattern::{IntoPatterns, Patterns};
|
||||
pub use self::quoter::Quoter;
|
||||
pub use self::resource::ResourceDef;
|
||||
pub use self::resource_path::{Resource, ResourcePath};
|
||||
pub use self::router::{ResourceInfo, Router, RouterBuilder};
|
||||
pub use self::router::{ResourceId, Router, RouterBuilder};
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
pub use self::url::Url;
|
||||
|
@ -64,10 +64,15 @@ impl Quoter {
|
||||
quoter
|
||||
}
|
||||
|
||||
/// Re-quotes... ?
|
||||
/// Decodes safe percent-encoded sequences from `val`.
|
||||
///
|
||||
/// Returns `None` when no modification to the original string was required.
|
||||
pub fn requote(&self, val: &[u8]) -> Option<String> {
|
||||
/// Returns `None` when no modification to the original byte string was required.
|
||||
///
|
||||
/// Non-ASCII bytes are accepted as valid input.
|
||||
///
|
||||
/// Behavior for invalid/incomplete percent-encoding sequences is unspecified and may include
|
||||
/// removing the invalid sequence from the output or passing it as-is.
|
||||
pub fn requote(&self, val: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut has_pct = 0;
|
||||
let mut pct = [b'%', 0, 0];
|
||||
let mut idx = 0;
|
||||
@ -121,7 +126,12 @@ impl Quoter {
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
cloned.map(|data| String::from_utf8_lossy(&data).into_owned())
|
||||
cloned
|
||||
}
|
||||
|
||||
pub(crate) fn requote_str_lossy(&self, val: &str) -> Option<String> {
|
||||
self.requote(val.as_bytes())
|
||||
.map(|data| String::from_utf8_lossy(&data).into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,14 +211,29 @@ mod tests {
|
||||
#[test]
|
||||
fn custom_quoter() {
|
||||
let q = Quoter::new(b"", b"+");
|
||||
assert_eq!(q.requote(b"/a%25c").unwrap(), "/a%c");
|
||||
assert_eq!(q.requote(b"/a%2Bc").unwrap(), "/a%2Bc");
|
||||
assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
|
||||
assert_eq!(q.requote(b"/a%2Bc").unwrap(), b"/a%2Bc");
|
||||
|
||||
let q = Quoter::new(b"%+", b"/");
|
||||
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c");
|
||||
assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb");
|
||||
assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb");
|
||||
assert_eq!(q.requote(b"/a%0Ab").unwrap(), "/a\nb");
|
||||
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
|
||||
assert_eq!(q.requote(b"/a%2fb").unwrap(), b"/a%2fb");
|
||||
assert_eq!(q.requote(b"/a%2Fb").unwrap(), b"/a%2Fb");
|
||||
assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb");
|
||||
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
||||
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ascii() {
|
||||
let q = Quoter::new(b"%+", b"/");
|
||||
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
||||
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_sequences() {
|
||||
let q = Quoter::new(b"%+", b"/");
|
||||
assert_eq!(q.requote(b"/a%2x%2X%%").unwrap(), b"/a%2x%2X");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -8,10 +8,7 @@ use std::{
|
||||
use firestorm::{profile_fn, profile_method, profile_section};
|
||||
use regex::{escape, Regex, RegexSet};
|
||||
|
||||
use crate::{
|
||||
path::{Path, PathItem},
|
||||
IntoPatterns, Patterns, Resource, ResourcePath,
|
||||
};
|
||||
use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
|
||||
|
||||
const MAX_DYNAMIC_SEGMENTS: usize = 16;
|
||||
|
||||
@ -615,7 +612,7 @@ impl ResourceDef {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects dynamic segment values into `path`.
|
||||
/// Collects dynamic segment values into `resource`.
|
||||
///
|
||||
/// Returns `true` if `path` matches this resource.
|
||||
///
|
||||
@ -635,9 +632,9 @@ impl ResourceDef {
|
||||
/// assert_eq!(path.get("path").unwrap(), "HEAD/Cargo.toml");
|
||||
/// assert_eq!(path.unprocessed(), "");
|
||||
/// ```
|
||||
pub fn capture_match_info<T: ResourcePath>(&self, path: &mut Path<T>) -> bool {
|
||||
pub fn capture_match_info<R: Resource>(&self, resource: &mut R) -> bool {
|
||||
profile_method!(capture_match_info);
|
||||
self.capture_match_info_fn(path, |_, _| true, ())
|
||||
self.capture_match_info_fn(resource, |_| true)
|
||||
}
|
||||
|
||||
/// Collects dynamic segment values into `resource` after matching paths and executing
|
||||
@ -655,13 +652,12 @@ impl ResourceDef {
|
||||
/// use actix_router::{Path, ResourceDef};
|
||||
///
|
||||
/// fn try_match(resource: &ResourceDef, path: &mut Path<&str>) -> bool {
|
||||
/// let admin_allowed = std::env::var("ADMIN_ALLOWED").ok();
|
||||
/// let admin_allowed = std::env::var("ADMIN_ALLOWED").is_ok();
|
||||
///
|
||||
/// resource.capture_match_info_fn(
|
||||
/// path,
|
||||
/// // when env var is not set, reject when path contains "admin"
|
||||
/// |res, admin_allowed| !res.path().contains("admin"),
|
||||
/// &admin_allowed
|
||||
/// |res| !(!admin_allowed && res.path().contains("admin")),
|
||||
/// )
|
||||
/// }
|
||||
///
|
||||
@ -678,15 +674,10 @@ impl ResourceDef {
|
||||
/// assert!(!try_match(&resource, &mut path));
|
||||
/// assert_eq!(path.unprocessed(), "/user/admin/stars");
|
||||
/// ```
|
||||
pub fn capture_match_info_fn<R, F, U>(
|
||||
&self,
|
||||
resource: &mut R,
|
||||
check_fn: F,
|
||||
user_data: U,
|
||||
) -> bool
|
||||
pub fn capture_match_info_fn<R, F>(&self, resource: &mut R, check_fn: F) -> bool
|
||||
where
|
||||
R: Resource,
|
||||
F: FnOnce(&R, U) -> bool,
|
||||
F: FnOnce(&R) -> bool,
|
||||
{
|
||||
profile_method!(capture_match_info_fn);
|
||||
|
||||
@ -762,7 +753,7 @@ impl ResourceDef {
|
||||
}
|
||||
};
|
||||
|
||||
if !check_fn(resource, user_data) {
|
||||
if !check_fn(resource) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -857,7 +848,7 @@ impl ResourceDef {
|
||||
S: BuildHasher,
|
||||
{
|
||||
profile_method!(resource_path_from_map);
|
||||
self.build_resource_path(path, |name| values.get(name).map(AsRef::<str>::as_ref))
|
||||
self.build_resource_path(path, |name| values.get(name))
|
||||
}
|
||||
|
||||
/// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`.
|
||||
@ -1157,6 +1148,7 @@ pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Path;
|
||||
|
||||
#[test]
|
||||
fn equivalence() {
|
||||
|
@ -5,87 +5,83 @@ use crate::{IntoPatterns, Resource, ResourceDef};
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct ResourceId(pub u16);
|
||||
|
||||
/// Information about current resource
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResourceInfo {
|
||||
#[allow(dead_code)]
|
||||
resource: ResourceId,
|
||||
}
|
||||
|
||||
/// Resource router.
|
||||
// T is the resource itself
|
||||
// U is any other data needed for routing like method guards
|
||||
///
|
||||
/// It matches a [routing resource](Resource) to an ordered list of _routes_. Each is defined by a
|
||||
/// single [`ResourceDef`] and contains two types of custom data:
|
||||
/// 1. The route _value_, of the generic type `T`.
|
||||
/// 1. Some _context_ data, of the generic type `U`, which is only provided to the check function in
|
||||
/// [`recognize_fn`](Self::recognize_fn). This parameter defaults to `()` and can be omitted if
|
||||
/// not required.
|
||||
pub struct Router<T, U = ()> {
|
||||
routes: Vec<(ResourceDef, T, Option<U>)>,
|
||||
routes: Vec<(ResourceDef, T, U)>,
|
||||
}
|
||||
|
||||
impl<T, U> Router<T, U> {
|
||||
/// Constructs new `RouterBuilder` with empty route list.
|
||||
pub fn build() -> RouterBuilder<T, U> {
|
||||
RouterBuilder {
|
||||
resources: Vec::new(),
|
||||
}
|
||||
RouterBuilder { routes: Vec::new() }
|
||||
}
|
||||
|
||||
/// Finds the value in the router that matches a given [routing resource](Resource).
|
||||
///
|
||||
/// The match result, including the captured dynamic segments, in the `resource`.
|
||||
pub fn recognize<R>(&self, resource: &mut R) -> Option<(&T, ResourceId)>
|
||||
where
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize);
|
||||
|
||||
for item in self.routes.iter() {
|
||||
if item.0.capture_match_info(resource.resource_path()) {
|
||||
return Some((&item.1, ResourceId(item.0.id())));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
self.recognize_fn(resource, |_, _| true)
|
||||
}
|
||||
|
||||
/// Same as [`recognize`](Self::recognize) but returns a mutable reference to the matched value.
|
||||
pub fn recognize_mut<R>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
|
||||
where
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize_mut);
|
||||
|
||||
for item in self.routes.iter_mut() {
|
||||
if item.0.capture_match_info(resource.resource_path()) {
|
||||
return Some((&mut item.1, ResourceId(item.0.id())));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
self.recognize_mut_fn(resource, |_, _| true)
|
||||
}
|
||||
|
||||
pub fn recognize_fn<R, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)>
|
||||
/// Finds the value in the router that matches a given [routing resource](Resource) and passes
|
||||
/// an additional predicate check using context data.
|
||||
///
|
||||
/// Similar to [`recognize`](Self::recognize). However, before accepting the route as matched,
|
||||
/// the `check` closure is executed, passing the resource and each route's context data. If the
|
||||
/// closure returns true then the match result is stored into `resource` and a reference to
|
||||
/// the matched _value_ is returned.
|
||||
pub fn recognize_fn<R, F>(&self, resource: &mut R, mut check: F) -> Option<(&T, ResourceId)>
|
||||
where
|
||||
F: Fn(&R, &Option<U>) -> bool,
|
||||
R: Resource,
|
||||
F: FnMut(&R, &U) -> bool,
|
||||
{
|
||||
profile_method!(recognize_checked);
|
||||
|
||||
for item in self.routes.iter() {
|
||||
if item.0.capture_match_info_fn(resource, &check, &item.2) {
|
||||
return Some((&item.1, ResourceId(item.0.id())));
|
||||
for (rdef, val, ctx) in self.routes.iter() {
|
||||
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
||||
return Some((val, ResourceId(rdef.id())));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Same as [`recognize_fn`](Self::recognize_fn) but returns a mutable reference to the matched
|
||||
/// value.
|
||||
pub fn recognize_mut_fn<R, F>(
|
||||
&mut self,
|
||||
resource: &mut R,
|
||||
check: F,
|
||||
mut check: F,
|
||||
) -> Option<(&mut T, ResourceId)>
|
||||
where
|
||||
F: Fn(&R, &Option<U>) -> bool,
|
||||
R: Resource,
|
||||
F: FnMut(&R, &U) -> bool,
|
||||
{
|
||||
profile_method!(recognize_mut_checked);
|
||||
|
||||
for item in self.routes.iter_mut() {
|
||||
if item.0.capture_match_info_fn(resource, &check, &item.2) {
|
||||
return Some((&mut item.1, ResourceId(item.0.id())));
|
||||
for (rdef, val, ctx) in self.routes.iter_mut() {
|
||||
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
||||
return Some((val, ResourceId(rdef.id())));
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,49 +89,69 @@ impl<T, U> Router<T, U> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for an ordered [routing](Router) list.
|
||||
pub struct RouterBuilder<T, U = ()> {
|
||||
resources: Vec<(ResourceDef, T, Option<U>)>,
|
||||
routes: Vec<(ResourceDef, T, U)>,
|
||||
}
|
||||
|
||||
impl<T, U> RouterBuilder<T, U> {
|
||||
/// Register resource for specified path.
|
||||
pub fn path<P: IntoPatterns>(
|
||||
/// Adds a new route to the end of the routing list.
|
||||
///
|
||||
/// Returns mutable references to elements of the new route.
|
||||
pub fn push(
|
||||
&mut self,
|
||||
path: P,
|
||||
resource: T,
|
||||
) -> &mut (ResourceDef, T, Option<U>) {
|
||||
profile_method!(path);
|
||||
|
||||
self.resources
|
||||
.push((ResourceDef::new(path), resource, None));
|
||||
self.resources.last_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Register resource for specified path prefix.
|
||||
pub fn prefix(&mut self, prefix: &str, resource: T) -> &mut (ResourceDef, T, Option<U>) {
|
||||
profile_method!(prefix);
|
||||
|
||||
self.resources
|
||||
.push((ResourceDef::prefix(prefix), resource, None));
|
||||
self.resources.last_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Register resource for ResourceDef
|
||||
pub fn rdef(&mut self, rdef: ResourceDef, resource: T) -> &mut (ResourceDef, T, Option<U>) {
|
||||
profile_method!(rdef);
|
||||
|
||||
self.resources.push((rdef, resource, None));
|
||||
self.resources.last_mut().unwrap()
|
||||
rdef: ResourceDef,
|
||||
val: T,
|
||||
ctx: U,
|
||||
) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||
profile_method!(push);
|
||||
self.routes.push((rdef, val, ctx));
|
||||
self.routes
|
||||
.last_mut()
|
||||
.map(|(rdef, val, ctx)| (rdef, val, ctx))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Finish configuration and create router instance.
|
||||
pub fn finish(self) -> Router<T, U> {
|
||||
Router {
|
||||
routes: self.resources,
|
||||
routes: self.routes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience methods provided when context data impls [`Default`]
|
||||
impl<T, U> RouterBuilder<T, U>
|
||||
where
|
||||
U: Default,
|
||||
{
|
||||
/// Registers resource for specified path.
|
||||
pub fn path(
|
||||
&mut self,
|
||||
path: impl IntoPatterns,
|
||||
val: T,
|
||||
) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||
profile_method!(path);
|
||||
self.push(ResourceDef::new(path), val, U::default())
|
||||
}
|
||||
|
||||
/// Registers resource for specified path prefix.
|
||||
pub fn prefix(
|
||||
&mut self,
|
||||
prefix: impl IntoPatterns,
|
||||
val: T,
|
||||
) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||
profile_method!(prefix);
|
||||
self.push(ResourceDef::prefix(prefix), val, U::default())
|
||||
}
|
||||
|
||||
/// Registers resource for [`ResourceDef`].
|
||||
pub fn rdef(&mut self, rdef: ResourceDef, val: T) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||
profile_method!(rdef);
|
||||
self.push(rdef, val, U::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::path::Path;
|
||||
|
@ -15,14 +15,14 @@ pub struct Url {
|
||||
impl Url {
|
||||
#[inline]
|
||||
pub fn new(uri: http::Uri) -> Url {
|
||||
let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes()));
|
||||
let path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path()));
|
||||
Url { uri, path }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
|
||||
Url {
|
||||
path: quoter.requote(uri.path().as_bytes()),
|
||||
path: quoter.requote_str_lossy(uri.path()),
|
||||
uri,
|
||||
}
|
||||
}
|
||||
@ -45,13 +45,13 @@ impl Url {
|
||||
#[inline]
|
||||
pub fn update(&mut self, uri: &http::Uri) {
|
||||
self.uri = uri.clone();
|
||||
self.path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes()));
|
||||
self.path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path()));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) {
|
||||
self.uri = uri.clone();
|
||||
self.path = quoter.requote(uri.path().as_bytes());
|
||||
self.path = quoter.requote_str_lossy(uri.path());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,12 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.1.0-beta.12 - 2022-01-31
|
||||
- Rename `TestServerConfig::{client_timeout => client_request_timeout}`. [#2611]
|
||||
|
||||
[#2611]: https://github.com/actix/actix-web/pull/2611
|
||||
|
||||
|
||||
## 0.1.0-beta.11 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-test"
|
||||
version = "0.1.0-beta.11"
|
||||
version = "0.1.0-beta.12"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.4.1"
|
||||
actix-http = "3.0.0-beta.19"
|
||||
actix-http-test = "3.0.0-beta.11"
|
||||
actix-http = "3.0.0-rc.1"
|
||||
actix-http-test = "3.0.0-beta.12"
|
||||
actix-rt = "2.1"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.21", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3.0.0-beta.19", default-features = false, features = ["cookies"] }
|
||||
actix-web = { version = "4.0.0-rc.1", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3.0.0-beta.20", default-features = false, features = ["cookies"] }
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
||||
|
@ -149,7 +149,7 @@ where
|
||||
let local_addr = tcp.local_addr().unwrap();
|
||||
let factory = factory.clone();
|
||||
let srv_cfg = cfg.clone();
|
||||
let timeout = cfg.client_timeout;
|
||||
let timeout = cfg.client_request_timeout;
|
||||
|
||||
let builder = Server::build().workers(1).disable_signals().system_exit();
|
||||
|
||||
@ -167,7 +167,7 @@ where
|
||||
.map_err(|err| err.into().error_response());
|
||||
|
||||
HttpService::build()
|
||||
.client_timeout(timeout)
|
||||
.client_request_timeout(timeout)
|
||||
.h1(map_config(fac, move |_| app_cfg.clone()))
|
||||
.tcp()
|
||||
}),
|
||||
@ -183,7 +183,7 @@ where
|
||||
.map_err(|err| err.into().error_response());
|
||||
|
||||
HttpService::build()
|
||||
.client_timeout(timeout)
|
||||
.client_request_timeout(timeout)
|
||||
.h2(map_config(fac, move |_| app_cfg.clone()))
|
||||
.tcp()
|
||||
}),
|
||||
@ -199,7 +199,7 @@ where
|
||||
.map_err(|err| err.into().error_response());
|
||||
|
||||
HttpService::build()
|
||||
.client_timeout(timeout)
|
||||
.client_request_timeout(timeout)
|
||||
.finish(map_config(fac, move |_| app_cfg.clone()))
|
||||
.tcp()
|
||||
}),
|
||||
@ -218,7 +218,7 @@ where
|
||||
.map_err(|err| err.into().error_response());
|
||||
|
||||
HttpService::build()
|
||||
.client_timeout(timeout)
|
||||
.client_request_timeout(timeout)
|
||||
.h1(map_config(fac, move |_| app_cfg.clone()))
|
||||
.openssl(acceptor.clone())
|
||||
}),
|
||||
@ -234,7 +234,7 @@ where
|
||||
.map_err(|err| err.into().error_response());
|
||||
|
||||
HttpService::build()
|
||||
.client_timeout(timeout)
|
||||
.client_request_timeout(timeout)
|
||||
.h2(map_config(fac, move |_| app_cfg.clone()))
|
||||
.openssl(acceptor.clone())
|
||||
}),
|
||||
@ -250,7 +250,7 @@ where
|
||||
.map_err(|err| err.into().error_response());
|
||||
|
||||
HttpService::build()
|
||||
.client_timeout(timeout)
|
||||
.client_request_timeout(timeout)
|
||||
.finish(map_config(fac, move |_| app_cfg.clone()))
|
||||
.openssl(acceptor.clone())
|
||||
}),
|
||||
@ -269,7 +269,7 @@ where
|
||||
.map_err(|err| err.into().error_response());
|
||||
|
||||
HttpService::build()
|
||||
.client_timeout(timeout)
|
||||
.client_request_timeout(timeout)
|
||||
.h1(map_config(fac, move |_| app_cfg.clone()))
|
||||
.rustls(config.clone())
|
||||
}),
|
||||
@ -285,7 +285,7 @@ where
|
||||
.map_err(|err| err.into().error_response());
|
||||
|
||||
HttpService::build()
|
||||
.client_timeout(timeout)
|
||||
.client_request_timeout(timeout)
|
||||
.h2(map_config(fac, move |_| app_cfg.clone()))
|
||||
.rustls(config.clone())
|
||||
}),
|
||||
@ -301,7 +301,7 @@ where
|
||||
.map_err(|err| err.into().error_response());
|
||||
|
||||
HttpService::build()
|
||||
.client_timeout(timeout)
|
||||
.client_request_timeout(timeout)
|
||||
.finish(map_config(fac, move |_| app_cfg.clone()))
|
||||
.rustls(config.clone())
|
||||
}),
|
||||
@ -388,7 +388,7 @@ pub fn config() -> TestServerConfig {
|
||||
pub struct TestServerConfig {
|
||||
tp: HttpVer,
|
||||
stream: StreamType,
|
||||
client_timeout: u64,
|
||||
client_request_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for TestServerConfig {
|
||||
@ -403,7 +403,7 @@ impl TestServerConfig {
|
||||
TestServerConfig {
|
||||
tp: HttpVer::Both,
|
||||
stream: StreamType::Tcp,
|
||||
client_timeout: 5000,
|
||||
client_request_timeout: Duration::from_secs(5),
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,9 +433,9 @@ impl TestServerConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set client timeout in milliseconds for first request.
|
||||
pub fn client_timeout(mut self, val: u64) -> Self {
|
||||
self.client_timeout = val;
|
||||
/// Set client timeout for first request.
|
||||
pub fn client_request_timeout(mut self, dur: Duration) -> Self {
|
||||
self.client_request_timeout = dur;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,10 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 4.0.0-beta.11 - 2022-01-31
|
||||
- No significant changes since `4.0.0-beta.10`.
|
||||
|
||||
|
||||
## 4.0.0-beta.10 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web-actors"
|
||||
version = "4.0.0-beta.10"
|
||||
version = "4.0.0-beta.11"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix actors support for Actix Web"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
@ -16,8 +16,8 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
actix = { version = "0.12.0", default-features = false }
|
||||
actix-codec = "0.4.1"
|
||||
actix-http = "3.0.0-beta.19"
|
||||
actix-web = { version = "4.0.0-beta.21", default-features = false }
|
||||
actix-http = "3.0.0-rc.1"
|
||||
actix-web = { version = "4.0.0-rc.1", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
@ -27,8 +27,8 @@ tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.11"
|
||||
awc = { version = "3.0.0-beta.19", default-features = false }
|
||||
actix-test = "0.1.0-beta.12"
|
||||
awc = { version = "3.0.0-beta.20", default-features = false }
|
||||
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Actix actors support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://docs.rs/actix-web-actors/4.0.0-beta.10)
|
||||
[](https://docs.rs/actix-web-actors/4.0.0-beta.11)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10)
|
||||
[](https://deps.rs/crate/actix-web-actors/4.0.0-beta.11)
|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -228,11 +228,10 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_default_resource() {
|
||||
let srv =
|
||||
init_service(App::new().service(web::resource("/test").to(|| {
|
||||
HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 }))
|
||||
})))
|
||||
.await;
|
||||
let srv = init_service(App::new().service(web::resource("/test").to(|| async {
|
||||
HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 }))
|
||||
})))
|
||||
.await;
|
||||
|
||||
let req = TestRequest::with_uri("/test").to_request();
|
||||
let resp = call_service(&srv, req).await;
|
||||
|
@ -3,6 +3,10 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.5.0-rc.2 - 2022-02-01
|
||||
- No significant changes since `0.5.0-rc.1`.
|
||||
|
||||
|
||||
## 0.5.0-rc.1 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web-codegen"
|
||||
version = "0.5.0-rc.1"
|
||||
version = "0.5.0-rc.2"
|
||||
description = "Routing and runtime macros for Actix Web"
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
@ -15,7 +15,7 @@ edition = "2018"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
actix-router = "0.5.0-beta.4"
|
||||
actix-router = "0.5.0-rc.3"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing"] }
|
||||
@ -23,9 +23,9 @@ syn = { version = "1", features = ["full", "parsing"] }
|
||||
[dev-dependencies]
|
||||
actix-macros = "0.2.3"
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.11"
|
||||
actix-test = "0.1.0-beta.12"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = "4.0.0-beta.21"
|
||||
actix-web = "4.0.0-rc.1"
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
trybuild = "1"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Routing and runtime macros for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-web-codegen)
|
||||
[](https://docs.rs/actix-web-codegen/0.5.0-rc.1)
|
||||
[](https://docs.rs/actix-web-codegen/0.5.0-rc.2)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1)
|
||||
[](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.2)
|
||||
[](https://crates.io/crates/actix-web-codegen)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -3,6 +3,10 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.20 - 2022-01-31
|
||||
- No significant changes since `3.0.0-beta.19`.
|
||||
|
||||
|
||||
## 3.0.0-beta.19 - 2022-01-21
|
||||
- No significant changes since `3.0.0-beta.18`.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "awc"
|
||||
version = "3.0.0-beta.19"
|
||||
version = "3.0.0-beta.20"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
@ -60,7 +60,7 @@ dangerous-h2c = []
|
||||
[dependencies]
|
||||
actix-codec = "0.4.1"
|
||||
actix-service = "2.0.0"
|
||||
actix-http = "3.0.0-beta.19"
|
||||
actix-http = { version = "3.0.0-rc.1", features = ["http2", "ws"] }
|
||||
actix-rt = { version = "2.1", default-features = false }
|
||||
actix-tls = { version = "3.0.0", features = ["connect", "uri"] }
|
||||
actix-utils = "3.0.0"
|
||||
@ -93,13 +93,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
|
||||
trust-dns-resolver = { version = "0.20.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http = { version = "3.0.0-beta.19", features = ["openssl"] }
|
||||
actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
|
||||
actix-http = { version = "3.0.0-rc.1", features = ["openssl"] }
|
||||
actix-http-test = { version = "3.0.0-beta.12", features = ["openssl"] }
|
||||
actix-server = "2"
|
||||
actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
|
||||
actix-test = { version = "0.1.0-beta.12", features = ["openssl", "rustls"] }
|
||||
actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] }
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.21", features = ["openssl"] }
|
||||
actix-web = { version = "4.0.0-rc.1", features = ["openssl"] }
|
||||
|
||||
brotli = "3.3.3"
|
||||
const-str = "0.3"
|
||||
|
@ -3,9 +3,9 @@
|
||||
> Async HTTP and WebSocket client library.
|
||||
|
||||
[](https://crates.io/crates/awc)
|
||||
[](https://docs.rs/awc/3.0.0-beta.19)
|
||||
[](https://docs.rs/awc/3.0.0-beta.20)
|
||||

|
||||
[](https://deps.rs/crate/awc/3.0.0-beta.19)
|
||||
[](https://deps.rs/crate/awc/3.0.0-beta.20)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
@ -70,7 +70,7 @@ where
|
||||
let is_expect = if head.as_ref().headers.contains_key(EXPECT) {
|
||||
match body.size() {
|
||||
BodySize::None | BodySize::Sized(0) => {
|
||||
let keep_alive = framed.codec_ref().keepalive();
|
||||
let keep_alive = framed.codec_ref().keep_alive();
|
||||
framed.io_mut().on_release(keep_alive);
|
||||
|
||||
// TODO: use a new variant or a new type better describing error violate
|
||||
@ -119,7 +119,7 @@ where
|
||||
|
||||
match pin_framed.codec_ref().message_type() {
|
||||
h1::MessageType::None => {
|
||||
let keep_alive = pin_framed.codec_ref().keepalive();
|
||||
let keep_alive = pin_framed.codec_ref().keep_alive();
|
||||
pin_framed.io_mut().on_release(keep_alive);
|
||||
|
||||
Ok((head, Payload::None))
|
||||
@ -223,7 +223,7 @@ impl<Io: ConnectionIo> Stream for PlStream<Io> {
|
||||
match ready!(this.framed.as_mut().next_item(cx)?) {
|
||||
Some(Some(chunk)) => Poll::Ready(Some(Ok(chunk))),
|
||||
Some(None) => {
|
||||
let keep_alive = this.framed.codec_ref().keepalive();
|
||||
let keep_alive = this.framed.codec_ref().keep_alive();
|
||||
this.framed.io_mut().on_release(keep_alive);
|
||||
Poll::Ready(None)
|
||||
}
|
||||
|
@ -94,10 +94,9 @@ impl Client {
|
||||
let mut req = ClientRequest::new(method, url, self.0.clone());
|
||||
|
||||
for header in self.0.default_headers.iter() {
|
||||
// header map is empty
|
||||
// TODO: probably append instead
|
||||
req = req.insert_header_if_none(header);
|
||||
req = req.append_header(header);
|
||||
}
|
||||
|
||||
req
|
||||
}
|
||||
|
||||
|
@ -163,7 +163,7 @@ where
|
||||
| StatusCode::PERMANENT_REDIRECT
|
||||
if *max_redirect_times > 0 =>
|
||||
{
|
||||
let is_redirect = res.head().status == StatusCode::TEMPORARY_REDIRECT
|
||||
let reuse_body = res.head().status == StatusCode::TEMPORARY_REDIRECT
|
||||
|| res.head().status == StatusCode::PERMANENT_REDIRECT;
|
||||
|
||||
let prev_uri = uri.take().unwrap();
|
||||
@ -176,7 +176,7 @@ where
|
||||
let connector = connector.take();
|
||||
|
||||
// reset method
|
||||
let method = if is_redirect {
|
||||
let method = if reuse_body {
|
||||
method.take().unwrap()
|
||||
} else {
|
||||
let method = method.take().unwrap();
|
||||
@ -187,18 +187,19 @@ where
|
||||
};
|
||||
|
||||
let mut body = body.take();
|
||||
let body_new = if is_redirect {
|
||||
// try to reuse body
|
||||
let body_new = if reuse_body {
|
||||
// try to reuse saved body
|
||||
match body {
|
||||
Some(ref bytes) => AnyBody::Bytes {
|
||||
body: bytes.clone(),
|
||||
},
|
||||
// TODO: should this be AnyBody::Empty or AnyBody::None.
|
||||
|
||||
// body was a non-reusable type so send an empty body instead
|
||||
_ => AnyBody::empty(),
|
||||
}
|
||||
} else {
|
||||
body = None;
|
||||
// remove body
|
||||
// remove body since we're downgrading to a GET
|
||||
AnyBody::None
|
||||
};
|
||||
|
||||
|
@ -30,7 +30,9 @@ const STR: &str = const_str::repeat!(S, 100);
|
||||
#[actix_rt::test]
|
||||
async fn test_simple() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
|
||||
App::new().service(
|
||||
web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })),
|
||||
)
|
||||
});
|
||||
|
||||
let request = srv.get("/").insert_header(("x-test", "111")).send();
|
||||
@ -93,7 +95,7 @@ async fn test_timeout() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| async {
|
||||
actix_rt::time::sleep(Duration::from_millis(200)).await;
|
||||
Ok::<_, Error>(HttpResponse::Ok().body(STR))
|
||||
HttpResponse::Ok().body(STR)
|
||||
})))
|
||||
});
|
||||
|
||||
@ -118,7 +120,7 @@ async fn test_timeout_override() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| async {
|
||||
actix_rt::time::sleep(Duration::from_millis(200)).await;
|
||||
Ok::<_, Error>(HttpResponse::Ok().body(STR))
|
||||
HttpResponse::Ok().body(STR)
|
||||
})))
|
||||
});
|
||||
|
||||
@ -295,10 +297,9 @@ async fn test_connection_server_close() {
|
||||
})
|
||||
.and_then(
|
||||
HttpService::new(map_config(
|
||||
App::new().service(
|
||||
web::resource("/")
|
||||
.route(web::to(|| HttpResponse::Ok().force_close().finish())),
|
||||
),
|
||||
App::new().service(web::resource("/").route(web::to(|| async {
|
||||
HttpResponse::Ok().force_close().finish()
|
||||
}))),
|
||||
|_| AppConfig::default(),
|
||||
))
|
||||
.tcp(),
|
||||
@ -336,7 +337,8 @@ async fn test_connection_wait_queue() {
|
||||
.and_then(
|
||||
HttpService::new(map_config(
|
||||
App::new().service(
|
||||
web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))),
|
||||
web::resource("/")
|
||||
.route(web::to(|| async { HttpResponse::Ok().body(STR) })),
|
||||
),
|
||||
|_| AppConfig::default(),
|
||||
))
|
||||
@ -383,10 +385,9 @@ async fn test_connection_wait_queue_force_close() {
|
||||
})
|
||||
.and_then(
|
||||
HttpService::new(map_config(
|
||||
App::new().service(
|
||||
web::resource("/")
|
||||
.route(web::to(|| HttpResponse::Ok().force_close().body(STR))),
|
||||
),
|
||||
App::new().service(web::resource("/").route(web::to(|| async {
|
||||
HttpResponse::Ok().force_close().body(STR)
|
||||
}))),
|
||||
|_| AppConfig::default(),
|
||||
))
|
||||
.tcp(),
|
||||
@ -445,7 +446,9 @@ async fn test_no_decompress() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new()
|
||||
.wrap(actix_web::middleware::Compress::default())
|
||||
.service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
|
||||
.service(
|
||||
web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })),
|
||||
)
|
||||
});
|
||||
|
||||
let mut res = awc::Client::new()
|
||||
@ -479,7 +482,7 @@ async fn test_no_decompress() {
|
||||
#[actix_rt::test]
|
||||
async fn test_client_gzip_encoding() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| async {
|
||||
HttpResponse::Ok()
|
||||
.insert_header(header::ContentEncoding::Gzip)
|
||||
.body(utils::gzip::encode(STR))
|
||||
@ -499,7 +502,7 @@ async fn test_client_gzip_encoding() {
|
||||
#[actix_rt::test]
|
||||
async fn test_client_gzip_encoding_large() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| async {
|
||||
HttpResponse::Ok()
|
||||
.insert_header(header::ContentEncoding::Gzip)
|
||||
.body(utils::gzip::encode(STR.repeat(10)))
|
||||
@ -525,7 +528,7 @@ async fn test_client_gzip_encoding_large_random() {
|
||||
.collect::<String>();
|
||||
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|data: Bytes| {
|
||||
App::new().service(web::resource("/").route(web::to(|data: Bytes| async {
|
||||
HttpResponse::Ok()
|
||||
.insert_header(header::ContentEncoding::Gzip)
|
||||
.body(utils::gzip::encode(data))
|
||||
@ -545,7 +548,7 @@ async fn test_client_gzip_encoding_large_random() {
|
||||
#[actix_rt::test]
|
||||
async fn test_client_brotli_encoding() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|data: Bytes| {
|
||||
App::new().service(web::resource("/").route(web::to(|data: Bytes| async {
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("content-encoding", "br"))
|
||||
.body(utils::brotli::encode(data))
|
||||
@ -571,10 +574,10 @@ async fn test_client_brotli_encoding_large_random() {
|
||||
.collect::<String>();
|
||||
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|data: Bytes| {
|
||||
App::new().service(web::resource("/").route(web::to(|data: Bytes| async {
|
||||
HttpResponse::Ok()
|
||||
.insert_header(header::ContentEncoding::Brotli)
|
||||
.body(utils::brotli::encode(&data))
|
||||
.body(utils::brotli::encode(data))
|
||||
})))
|
||||
});
|
||||
|
||||
@ -590,7 +593,9 @@ async fn test_client_brotli_encoding_large_random() {
|
||||
#[actix_rt::test]
|
||||
async fn test_client_deflate_encoding() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body)))
|
||||
App::new().default_service(web::to(|body: Bytes| async {
|
||||
HttpResponse::Ok().body(body)
|
||||
}))
|
||||
});
|
||||
|
||||
let req = srv
|
||||
@ -614,7 +619,9 @@ async fn test_client_deflate_encoding_large_random() {
|
||||
.collect::<String>();
|
||||
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body)))
|
||||
App::new().default_service(web::to(|body: Bytes| async {
|
||||
HttpResponse::Ok().body(body)
|
||||
}))
|
||||
});
|
||||
|
||||
let req = srv
|
||||
@ -632,7 +639,7 @@ async fn test_client_deflate_encoding_large_random() {
|
||||
#[actix_rt::test]
|
||||
async fn test_client_streaming_explicit() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().default_service(web::to(|body: web::Payload| {
|
||||
App::new().default_service(web::to(|body: web::Payload| async {
|
||||
HttpResponse::Ok().streaming(body)
|
||||
}))
|
||||
});
|
||||
@ -654,7 +661,7 @@ async fn test_client_streaming_explicit() {
|
||||
#[actix_rt::test]
|
||||
async fn test_body_streaming_implicit() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().default_service(web::to(|| {
|
||||
App::new().default_service(web::to(|| async {
|
||||
let body =
|
||||
stream::once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_bytes())) });
|
||||
HttpResponse::Ok().streaming(body)
|
||||
|
@ -33,8 +33,9 @@ fn bench_async_burst(c: &mut Criterion) {
|
||||
|
||||
let srv = rt.block_on(async {
|
||||
actix_test::start(|| {
|
||||
App::new()
|
||||
.service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
|
||||
App::new().service(
|
||||
web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
|
3
examples/README.md
Normal file
3
examples/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Actix Web Examples
|
||||
|
||||
This folder contain just a few standalone code samples. There is a much larger registry of example projects [in the examples repo](https://github.com/actix/examples).
|
@ -13,16 +13,26 @@ save_exit_code() {
|
||||
}
|
||||
|
||||
save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture
|
||||
# save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
|
||||
# save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
# save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
|
||||
# save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
|
||||
# save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
|
||||
# save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture
|
||||
# save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture
|
||||
# save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture
|
||||
# save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture
|
||||
|
||||
save_exit_code cargo test --workspace --doc
|
||||
# save_exit_code cargo test --workspace --doc
|
||||
|
||||
if [ "$EXIT" = "0" ]; then
|
||||
PASSED="All tests passed!"
|
||||
|
||||
if [ "$(command -v figlet)" ]; then
|
||||
figlet "$PASSED"
|
||||
else
|
||||
echo "$PASSED"
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $EXIT
|
||||
|
27
src/app.rs
27
src/app.rs
@ -236,10 +236,14 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Default service to be used if no matching resource could be found.
|
||||
/// Default service that is invoked when no matching resource could be found.
|
||||
///
|
||||
/// It is possible to use services like `Resource`, `Route`.
|
||||
/// You can use a [`Route`] as default service.
|
||||
///
|
||||
/// If a default service is not registered, an empty `404 Not Found` response will be sent to
|
||||
/// the client instead.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{web, App, HttpResponse};
|
||||
///
|
||||
@ -248,23 +252,8 @@ where
|
||||
/// }
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .service(
|
||||
/// web::resource("/index.html").route(web::get().to(index)))
|
||||
/// .default_service(
|
||||
/// web::route().to(|| HttpResponse::NotFound()));
|
||||
/// ```
|
||||
///
|
||||
/// It is also possible to use static files as default service.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{web, App, HttpResponse};
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .service(
|
||||
/// web::resource("/index.html").to(|| HttpResponse::Ok()))
|
||||
/// .default_service(
|
||||
/// web::to(|| HttpResponse::NotFound())
|
||||
/// );
|
||||
/// .service(web::resource("/index.html").route(web::get().to(index)))
|
||||
/// .default_service(web::to(|| HttpResponse::NotFound()));
|
||||
/// ```
|
||||
pub fn default_service<F, U>(mut self, svc: F) -> Self
|
||||
where
|
||||
|
@ -21,8 +21,6 @@ use crate::{
|
||||
Error, HttpResponse,
|
||||
};
|
||||
|
||||
type Guards = Vec<Box<dyn Guard>>;
|
||||
|
||||
/// Service factory to convert `Request` to a `ServiceRequest<S>`.
|
||||
///
|
||||
/// It also executes data factories.
|
||||
@ -72,7 +70,7 @@ where
|
||||
})))
|
||||
});
|
||||
|
||||
// App config
|
||||
// create App config to pass to child services
|
||||
let mut config = AppService::new(config, default.clone());
|
||||
|
||||
// register services
|
||||
@ -169,7 +167,6 @@ impl AppInitServiceState {
|
||||
Rc::new(AppInitServiceState {
|
||||
rmap,
|
||||
config,
|
||||
// TODO: AppConfig can be used to pass user defined HttpRequestPool capacity.
|
||||
pool: HttpRequestPool::default(),
|
||||
})
|
||||
}
|
||||
@ -245,7 +242,7 @@ pub struct AppRoutingFactory {
|
||||
[(
|
||||
ResourceDef,
|
||||
BoxedHttpServiceFactory,
|
||||
RefCell<Option<Guards>>,
|
||||
RefCell<Option<Vec<Box<dyn Guard>>>>,
|
||||
)],
|
||||
>,
|
||||
default: Rc<BoxedHttpServiceFactory>,
|
||||
@ -263,7 +260,7 @@ impl ServiceFactory<ServiceRequest> for AppRoutingFactory {
|
||||
// construct all services factory future with it's resource def and guards.
|
||||
let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| {
|
||||
let path = path.clone();
|
||||
let guards = guards.borrow_mut().take();
|
||||
let guards = guards.borrow_mut().take().unwrap_or_default();
|
||||
let factory_fut = factory.new_service(());
|
||||
async move {
|
||||
let service = factory_fut.await?;
|
||||
@ -284,7 +281,7 @@ impl ServiceFactory<ServiceRequest> for AppRoutingFactory {
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.drain(..)
|
||||
.fold(Router::build(), |mut router, (path, guards, service)| {
|
||||
router.rdef(path, service).2 = guards;
|
||||
router.push(path, service, guards);
|
||||
router
|
||||
})
|
||||
.finish();
|
||||
@ -296,7 +293,7 @@ impl ServiceFactory<ServiceRequest> for AppRoutingFactory {
|
||||
|
||||
/// The Actix Web router default entry point.
|
||||
pub struct AppRouting {
|
||||
router: Router<BoxedHttpService, Guards>,
|
||||
router: Router<BoxedHttpService, Vec<Box<dyn Guard>>>,
|
||||
default: BoxedHttpService,
|
||||
}
|
||||
|
||||
@ -309,17 +306,8 @@ impl Service<ServiceRequest> for AppRouting {
|
||||
|
||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||
let res = self.router.recognize_fn(&mut req, |req, guards| {
|
||||
if let Some(ref guards) = guards {
|
||||
let guard_ctx = req.guard_ctx();
|
||||
|
||||
for guard in guards {
|
||||
if !guard.check(&guard_ctx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
let guard_ctx = req.guard_ctx();
|
||||
guards.iter().all(|guard| guard.check(&guard_ctx))
|
||||
});
|
||||
|
||||
if let Some((srv, _info)) = res {
|
||||
|
@ -4,16 +4,14 @@ use actix_http::{body::BoxBody, Response};
|
||||
|
||||
use crate::{HttpResponse, ResponseError};
|
||||
|
||||
/// General purpose actix web error.
|
||||
/// General purpose Actix Web error.
|
||||
///
|
||||
/// An actix web error is used to carry errors from `std::error`
|
||||
/// through actix in a convenient way. It can be created through
|
||||
/// converting errors with `into()`.
|
||||
/// An Actix Web error is used to carry errors from `std::error` through actix in a convenient way.
|
||||
/// It can be created through converting errors with `into()`.
|
||||
///
|
||||
/// Whenever it is created from an external object a response error is created
|
||||
/// for it that can be used to create an HTTP response from it this means that
|
||||
/// if you have access to an actix `Error` you can always get a
|
||||
/// `ResponseError` reference from it.
|
||||
/// Whenever it is created from an external object a response error is created for it that can be
|
||||
/// used to create an HTTP response from it this means that if you have access to an actix `Error`
|
||||
/// you can always get a `ResponseError` reference from it.
|
||||
pub struct Error {
|
||||
cause: Box<dyn ResponseError>,
|
||||
}
|
||||
|
12
src/guard.rs
12
src/guard.rs
@ -373,7 +373,9 @@ impl Guard for HeaderGuard {
|
||||
///
|
||||
/// web::scope("/admin")
|
||||
/// .guard(Host("admin.rust-lang.org").scheme("https"))
|
||||
/// .default_service(web::to(|| HttpResponse::Ok().body("admin connection is secure")));
|
||||
/// .default_service(web::to(|| async {
|
||||
/// HttpResponse::Ok().body("admin connection is secure")
|
||||
/// }));
|
||||
/// ```
|
||||
///
|
||||
/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app.
|
||||
@ -388,12 +390,16 @@ impl Guard for HeaderGuard {
|
||||
/// .service(
|
||||
/// web::scope("")
|
||||
/// .guard(guard::Host("www.rust-lang.org"))
|
||||
/// .default_service(web::to(|| HttpResponse::Ok().body("marketing site"))),
|
||||
/// .default_service(web::to(|| async {
|
||||
/// HttpResponse::Ok().body("marketing site")
|
||||
/// })),
|
||||
/// )
|
||||
/// .service(
|
||||
/// web::scope("")
|
||||
/// .guard(guard::Host("play.rust-lang.org"))
|
||||
/// .default_service(web::to(|| HttpResponse::Ok().body("playground frontend"))),
|
||||
/// .default_service(web::to(|| async {
|
||||
/// HttpResponse::Ok().body("playground frontend")
|
||||
/// })),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
|
@ -52,7 +52,7 @@ use crate::{
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .wrap(middleware::Compress::default())
|
||||
/// .default_service(web::to(|| HttpResponse::Ok().body("hello world")));
|
||||
/// .default_service(web::to(|| async { HttpResponse::Ok().body("hello world") }));
|
||||
/// ```
|
||||
///
|
||||
/// Pre-compressed Gzip file being served from disk with correct headers added to bypass middleware:
|
||||
|
@ -129,10 +129,11 @@ impl HttpRequest {
|
||||
/// later in a request handler to access the matched value for that parameter.
|
||||
///
|
||||
/// # Percent Encoding and URL Parameters
|
||||
/// Because each URL parameter is able to capture multiple path segments, both `["%2F", "%25"]`
|
||||
/// found in the request URI are not decoded into `["/", "%"]` in order to preserve path
|
||||
/// segment boundaries. If a url parameter is expected to contain these characters, then it is
|
||||
/// on the user to decode them.
|
||||
/// Because each URL parameter is able to capture multiple path segments, none of
|
||||
/// `["%2F", "%25", "%2B"]` found in the request URI are decoded into `["/", "%", "+"]` in order
|
||||
/// to preserve path integrity. If a URL parameter is expected to contain these characters, then
|
||||
/// it is on the user to decode them or use the [`web::Path`](crate::web::Path) extractor which
|
||||
/// _will_ decode these special sequences.
|
||||
#[inline]
|
||||
pub fn match_info(&self) -> &Path<Url> {
|
||||
&self.inner.path
|
||||
@ -504,12 +505,11 @@ impl HttpRequestPool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_service::Service;
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
dev::{ResourceDef, ResourceMap},
|
||||
dev::{ResourceDef, ResourceMap, Service},
|
||||
http::{header, StatusCode},
|
||||
test::{self, call_service, init_service, read_body, TestRequest},
|
||||
web, App, HttpResponse,
|
||||
@ -902,6 +902,7 @@ mod tests {
|
||||
// `body` equals http://localhost:8080/bar/nested
|
||||
// because nested from /bar overrides /foo's
|
||||
// to do this any other way would require something like a custom tree search
|
||||
// see https://github.com/actix/actix-web/issues/1763
|
||||
assert_eq!(body, "http://localhost:8080/bar/nested");
|
||||
|
||||
let bar_resp =
|
||||
|
@ -206,10 +206,10 @@ where
|
||||
/// Register a new route and add handler. This route matches all requests.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::*;
|
||||
/// use actix_web::{App, HttpRequest, HttpResponse, web};
|
||||
///
|
||||
/// fn index(req: HttpRequest) -> HttpResponse {
|
||||
/// unimplemented!()
|
||||
/// async fn index(req: HttpRequest) -> HttpResponse {
|
||||
/// todo!()
|
||||
/// }
|
||||
///
|
||||
/// App::new().service(web::resource("/").to(index));
|
||||
@ -219,7 +219,7 @@ where
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web::*;
|
||||
/// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() }
|
||||
/// # async fn index(req: HttpRequest) -> HttpResponse { todo!() }
|
||||
/// App::new().service(web::resource("/").route(web::route().to(index)));
|
||||
/// ```
|
||||
pub fn to<F, Args>(mut self, handler: F) -> Self
|
||||
@ -313,8 +313,12 @@ where
|
||||
}
|
||||
|
||||
/// Default service to be used if no matching route could be found.
|
||||
/// By default *405* response get returned. Resource does not use
|
||||
/// default handler from `App` or `Scope`.
|
||||
///
|
||||
/// You can use a [`Route`] as default service.
|
||||
///
|
||||
/// If a default service is not registered, an empty `405 Method Not Allowed` response will be
|
||||
/// sent to the client instead. Unlike [`Scope`](crate::Scope)s, a [`Resource`] does **not**
|
||||
/// inherit its parent's default service.
|
||||
pub fn default_service<F, U>(mut self, f: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<U, ServiceRequest>,
|
||||
|
@ -1,10 +1,6 @@
|
||||
use std::{
|
||||
cell::{Ref, RefMut},
|
||||
fmt,
|
||||
future::Future,
|
||||
mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_http::{
|
||||
@ -242,7 +238,7 @@ impl<B> HttpResponse<B> {
|
||||
(
|
||||
HttpResponse {
|
||||
res: head,
|
||||
error: None,
|
||||
error: self.error,
|
||||
},
|
||||
body,
|
||||
)
|
||||
@ -252,11 +248,11 @@ impl<B> HttpResponse<B> {
|
||||
pub fn drop_body(self) -> HttpResponse<()> {
|
||||
HttpResponse {
|
||||
res: self.res.drop_body(),
|
||||
error: None,
|
||||
error: self.error,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the current body type to another using a closure. Returns a new response.
|
||||
/// Map the current body type to another using a closure, returning a new response.
|
||||
///
|
||||
/// Closure receives the response head and the current body type.
|
||||
pub fn map_body<F, B2>(self, f: F) -> HttpResponse<B2>
|
||||
@ -269,18 +265,23 @@ impl<B> HttpResponse<B> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: docs for the body map methods below
|
||||
|
||||
/// Map the current body type `B` to `EitherBody::Left(B)`.
|
||||
///
|
||||
/// Useful for middleware which can generate their own responses.
|
||||
#[inline]
|
||||
pub fn map_into_left_body<R>(self) -> HttpResponse<EitherBody<B, R>> {
|
||||
self.map_body(|_, body| EitherBody::left(body))
|
||||
}
|
||||
|
||||
/// Map the current body type `B` to `EitherBody::Right(B)`.
|
||||
///
|
||||
/// Useful for middleware which can generate their own responses.
|
||||
#[inline]
|
||||
pub fn map_into_right_body<L>(self) -> HttpResponse<EitherBody<L, B>> {
|
||||
self.map_body(|_, body| EitherBody::right(body))
|
||||
}
|
||||
|
||||
/// Map the current body to a type-erased `BoxBody`.
|
||||
#[inline]
|
||||
pub fn map_into_boxed_body(self) -> HttpResponse<BoxBody>
|
||||
where
|
||||
@ -289,7 +290,7 @@ impl<B> HttpResponse<B> {
|
||||
self.map_body(|_, body| body.boxed())
|
||||
}
|
||||
|
||||
/// Extract response body
|
||||
/// Returns the response body, dropping all other parts.
|
||||
pub fn into_body(self) -> B {
|
||||
self.res.into_body()
|
||||
}
|
||||
@ -332,24 +333,39 @@ impl<B> From<HttpResponse<B>> for Response<B> {
|
||||
}
|
||||
}
|
||||
|
||||
// Future is only implemented for BoxBody payload type because it's the most useful for making
|
||||
// simple handlers without async blocks. Making it generic over all MessageBody types requires a
|
||||
// future impl on Response which would cause it's body field to be, undesirably, Option<B>.
|
||||
//
|
||||
// This impl is not particularly efficient due to the Response construction and should probably
|
||||
// not be invoked if performance is important. Prefer an async fn/block in such cases.
|
||||
impl Future for HttpResponse<BoxBody> {
|
||||
type Output = Result<Response<BoxBody>, Error>;
|
||||
// Rationale for cfg(test): this impl causes false positives on a clippy lint (async_yields_async)
|
||||
// when returning an HttpResponse from an async function/closure and it's not very useful outside of
|
||||
// tests anyway.
|
||||
#[cfg(test)]
|
||||
mod response_fut_impl {
|
||||
use std::{
|
||||
future::Future,
|
||||
mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if let Some(err) = self.error.take() {
|
||||
return Poll::Ready(Err(err));
|
||||
use super::*;
|
||||
|
||||
// Future is only implemented for BoxBody payload type because it's the most useful for making
|
||||
// simple handlers without async blocks. Making it generic over all MessageBody types requires a
|
||||
// future impl on Response which would cause it's body field to be, undesirably, Option<B>.
|
||||
//
|
||||
// This impl is not particularly efficient due to the Response construction and should probably
|
||||
// not be invoked if performance is important. Prefer an async fn/block in such cases.
|
||||
impl Future for HttpResponse<BoxBody> {
|
||||
type Output = Result<Response<BoxBody>, Error>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if let Some(err) = self.error.take() {
|
||||
return Poll::Ready(Err(err));
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(mem::replace(
|
||||
&mut self.res,
|
||||
Response::new(StatusCode::default()),
|
||||
)))
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(mem::replace(
|
||||
&mut self.res,
|
||||
Response::new(StatusCode::default()),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
22
src/scope.rs
22
src/scope.rs
@ -262,9 +262,10 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Default service to be used if no matching route could be found.
|
||||
/// Default service to be used if no matching resource could be found.
|
||||
///
|
||||
/// If default resource is not registered, app's default resource is being used.
|
||||
/// If a default service is not registered, it will fall back to the default service of
|
||||
/// the parent [`App`](crate::App) (see [`App::default_service`](crate::App::default_service)).
|
||||
pub fn default_service<F, U>(mut self, f: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<U, ServiceRequest>,
|
||||
@ -466,7 +467,7 @@ impl ServiceFactory<ServiceRequest> for ScopeFactory {
|
||||
// construct all services factory future with it's resource def and guards.
|
||||
let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| {
|
||||
let path = path.clone();
|
||||
let guards = guards.borrow_mut().take();
|
||||
let guards = guards.borrow_mut().take().unwrap_or_default();
|
||||
let factory_fut = factory.new_service(());
|
||||
async move {
|
||||
let service = factory_fut.await?;
|
||||
@ -484,7 +485,7 @@ impl ServiceFactory<ServiceRequest> for ScopeFactory {
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.drain(..)
|
||||
.fold(Router::build(), |mut router, (path, guards, service)| {
|
||||
router.rdef(path, service).2 = guards;
|
||||
router.push(path, service, guards);
|
||||
router
|
||||
})
|
||||
.finish();
|
||||
@ -508,17 +509,8 @@ impl Service<ServiceRequest> for ScopeService {
|
||||
|
||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||
let res = self.router.recognize_fn(&mut req, |req, guards| {
|
||||
if let Some(ref guards) = guards {
|
||||
let guard_ctx = req.guard_ctx();
|
||||
|
||||
for guard in guards {
|
||||
if !guard.check(&guard_ctx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
let guard_ctx = req.guard_ctx();
|
||||
guards.iter().all(|guard| guard.check(&guard_ctx))
|
||||
});
|
||||
|
||||
if let Some((srv, _info)) = res {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user