1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-04 18:06:23 +02:00

Compare commits

...

72 Commits

Author SHA1 Message Date
5469b02638 prepare actix-web-codegen release 0.5.0-rc.2 2022-02-01 00:12:42 +00:00
a66cd38ec5 prepare actix-web-actors release 4.0.0-beta.11 2022-01-31 22:35:18 +00:00
20609e93fd prepare actix-test release 0.1.0-beta.12 2022-01-31 22:34:59 +00:00
bf282472ab prepare actix-http-test release 3.0.0-beta.12 2022-01-31 22:33:38 +00:00
7f4b44c258 prepare actix-multipart release 0.4.0-beta.13 2022-01-31 22:33:11 +00:00
66243717b3 prepare actix-files release 0.6.0-beta.16 2022-01-31 22:32:52 +00:00
102720d398 prepare awc release 3.0.0-beta.20 2022-01-31 22:32:09 +00:00
c3c7eb8df9 prepare actix-web release 4.0.0-rc.1 2022-01-31 22:23:33 +00:00
21f57caf4a prepare actix-http release 3.0.0-rc.1 2022-01-31 22:22:40 +00:00
47f5faf26e prepare actix-router release 0.5.0-rc.3 2022-01-31 22:21:30 +00:00
9777653dc0 prep readme for rc release 2022-01-31 22:20:53 +00:00
9fde5b30db tweak and document router (#2612)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-31 22:12:48 +00:00
fd412a8223 Quoter::requote returns Vec<u8> (#2613) 2022-01-31 21:26:34 +00:00
cd511affd5 add ws and http2 feature flags (#2618) 2022-01-31 21:22:23 +00:00
3200de3f34 fix request head timeout (#2611) 2022-01-31 17:30:34 +00:00
b3e84b5c4b tweak default_service docs 2022-01-28 20:53:51 +00:00
a3416112a5 Improve the documentation for default_service (#2614) 2022-01-28 20:31:54 +00:00
21a08ca796 tweak set_content_encoding docs 2022-01-28 20:27:16 +00:00
a9f497d05f Guard against broken intra-doc links in CI (#2616) 2022-01-28 17:28:16 +00:00
cc9ba162f7 add late request dispatcher test 2022-01-27 17:00:07 +00:00
37799df978 add basic dispatcher test 2022-01-27 06:42:54 +00:00
0d93a8c273 add examples readme 2022-01-27 06:32:28 +00:00
3ae4f0a629 add keep-alive dispatcher tests 2022-01-27 06:29:46 +00:00
14a4f325d3 move dispatcher tests to own file 2022-01-27 06:06:55 +00:00
1bd2076b35 prevent drive traversal in windows 2022-01-25 16:44:05 +00:00
5454699bab propagate response error in all necessary places 2022-01-24 11:56:23 +00:00
d7c5c966d2 remove impl Future for HttpResponse (#2601) 2022-01-24 11:56:01 +00:00
50894e392e document new body map types 2022-01-23 23:26:35 +00:00
008753f07a improve body docs 2022-01-23 03:57:08 +00:00
c92aa31f91 document full percent-decoding of web::Path 2022-01-22 16:17:46 +00:00
c25dd23820 move path impls to derives 2022-01-22 04:02:34 +00:00
acacb90b2e add actix-http 2.2.2 changelog 2022-01-21 21:24:09 +00:00
8459f566a8 fix brotli encoding buffer size 2022-01-21 21:17:07 +00:00
232a14dc8b prepare actix-files release 0.6.0-beta.15 2022-01-21 20:27:29 +00:00
6e9f5fba24 prepare awc release 3.0.0-beta.19 2022-01-21 20:25:46 +00:00
c5d6df0078 prepare actix-web release 4.0.0-beta.21 2022-01-21 20:23:29 +00:00
8865540f3b prepare actix-http release 3.0.0-beta.19 2022-01-21 20:21:49 +00:00
141790b200 use camel case in special headers
fixes #2595
2022-01-21 20:15:43 +00:00
9668a2396f prepare actix-router release 0.5.0-rc.2 2022-01-21 17:21:46 +00:00
cb7347216c add Logger::log_target (#2594) 2022-01-21 17:19:17 +00:00
ae7f71e317 remove ambiguous HttpResponseBuilder::del_cookie (#2591) 2022-01-21 17:18:07 +00:00
bc89f0bfc2 s/example/examples 2022-01-21 16:56:33 +00:00
c959916346 fmt codegen 2022-01-20 01:54:57 +00:00
f227e880d7 refactor route codegen to be cleaner 2022-01-20 01:53:02 +00:00
68ad81f989 remove debug logs 2022-01-20 01:30:33 +00:00
f2e736719a add url_for test for conflicting named resources 2022-01-20 01:30:33 +00:00
81ef12a0fd add warn log to from_parts if given request is cloned
closes #2562
2022-01-19 22:23:53 +00:00
1bc1538118 use tokio::main in client example 2022-01-19 21:36:14 +00:00
1cc3e7b24c deprecate Path::path (#2590) 2022-01-19 20:26:33 +00:00
3dd98c308c document Path::unprocessed panic 2022-01-19 18:33:23 +00:00
cb5d9a7e64 bump deps to stable actix-server v2 2022-01-19 16:58:11 +00:00
5ee555462f add HttpResponse::add_removal_cookie (#2586) 2022-01-19 16:36:11 +00:00
ad159f5219 fix ClientResponse::body doc
fixes #2589
2022-01-19 15:52:16 +00:00
2ffc21dd4f move response extensions out of head (#2585) 2022-01-19 02:09:25 +00:00
7b8a392ef5 allow camel case response headers (#2587) 2022-01-16 03:16:26 +00:00
3c7ccf5521 update http changelog 2022-01-15 15:43:18 +00:00
e7cae5a95b migrate to brotli crate (#2538) 2022-01-15 14:03:16 +00:00
455d5c460d prepare actix-files release 0.6.0-beta.14 2022-01-14 20:01:11 +00:00
8faca783fa prepare actix-web release 4.0.0-beta.20 2022-01-14 20:00:26 +00:00
edbb9b047e prepare actix-router release 0.5.0-rc.1 2022-01-14 19:59:36 +00:00
32742d0715 support opaque app in test helpers (#2584) 2022-01-14 19:45:32 +00:00
d90c1a2331 convert error in Result extractor (#2581)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-12 18:59:22 +00:00
2a12b41456 fix support for 12 extractors (#2582)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-12 18:31:48 +00:00
6c97d448b7 update tokio-uring to 0.2 (#2583) 2022-01-12 17:53:36 +00:00
c3ce33df05 unify generics across App, Scope and Resource (#2572)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 15:02:28 +00:00
4431c8da65 fix bench 2022-01-05 14:10:38 +00:00
2d11ab5977 Add ServiceConfig::configure (#1988)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 12:31:39 +00:00
4ebf16890d add GuardContext::header (#2569) 2022-01-05 11:47:14 +00:00
fe0bbfb3da optimize PathDeserializer (#2570) 2022-01-05 10:48:20 +00:00
2462b6dd5d generalize impl Responder for HttpResponse (#2567)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 04:42:52 +00:00
49cfabeaf5 simplify Resource trait (#2568)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 04:34:13 +00:00
0f7292c69a remove readme msrv link 2022-01-05 04:24:40 +00:00
147 changed files with 4359 additions and 2700 deletions

View File

@ -46,3 +46,21 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --examples --all-features 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

View File

@ -3,6 +3,53 @@
## Unreleased - 2021-xx-xx ## 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]
- `Logger::log_target`. [#2594]
### Removed
- `HttpRequest::req_data[_mut]()`; request-local data is still available through `.extensions()`. [#2585]
- `HttpRequestBuilder::del_cookie`. [#2591]
[#2585]: https://github.com/actix/actix-web/pull/2585
[#2586]: https://github.com/actix/actix-web/pull/2586
[#2591]: https://github.com/actix/actix-web/pull/2591
[#2594]: https://github.com/actix/actix-web/pull/2594
## 4.0.0-beta.20 - 2022-01-14
### Added
- `GuardContext::header` [#2569]
- `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988]
### Changed
- `HttpResponse` can now be used as a `Responder` with any body type. [#2567]
- `Result` extractor wrapper can now convert error types. [#2581]
- Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581]
- Maximum number of handler extractors has increased to 12. [#2582]
- Removed bound `<B as MessageBody>::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584]
[#1988]: https://github.com/actix/actix-web/pull/1988
[#2567]: https://github.com/actix/actix-web/pull/2567
[#2569]: https://github.com/actix/actix-web/pull/2569
[#2581]: https://github.com/actix/actix-web/pull/2581
[#2582]: https://github.com/actix/actix-web/pull/2582
[#2584]: https://github.com/actix/actix-web/pull/2584
## 4.0.0-beta.19 - 2022-01-04 ## 4.0.0-beta.19 - 2022-01-04
### Added ### Added
- `impl Hash` for `http::header::Encoding`. [#2501] - `impl Hash` for `http::header::Encoding`. [#2501]

View File

@ -1,7 +1,10 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.19" version = "4.0.0-rc.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
categories = [ categories = [
@ -71,15 +74,15 @@ experimental-io-uring = ["actix-server/io-uring"]
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-rt = "2.3" actix-rt = "2.6"
actix-server = "2.0.0-rc.2" actix-server = "2"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-tls = { version = "3.0.0", default-features = false, optional = true } actix-tls = { version = "3.0.0", default-features = false, optional = true }
actix-http = "3.0.0-beta.18" actix-http = { version = "3.0.0-rc.1", features = ["http2", "ws"] }
actix-router = "0.5.0-beta.4" actix-router = "0.5.0-rc.3"
actix-web-codegen = "0.5.0-rc.1" actix-web-codegen = "0.5.0-rc.2"
ahash = "0.7" ahash = "0.7"
bytes = "1" bytes = "1"
@ -105,11 +108,11 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1" url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-files = "0.6.0-beta.13" actix-files = "0.6.0-beta.16"
actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.12", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.18", features = ["openssl"] } awc = { version = "3.0.0-beta.20", features = ["openssl"] }
brotli2 = "0.3.2" brotli = "3.3.3"
const-str = "0.3" const-str = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9" env_logger = "0.9"
@ -118,6 +121,7 @@ futures-util = { version = "0.3.7", default-features = false, features = ["std"]
rand = "0.8" rand = "0.8"
rcgen = "0.8" rcgen = "0.8"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" } tls-rustls = { package = "rustls", version = "0.20.0" }
zstd = "0.9" zstd = "0.9"
@ -156,6 +160,10 @@ awc = { path = "awc" }
name = "test_server" name = "test_server"
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
[[test]]
name = "compression"
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
[[example]] [[example]]
name = "basic" name = "basic"
required-features = ["compress-gzip"] required-features = ["compress-gzip"]

View File

@ -6,10 +6,10 @@
<p> <p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.19)](https://docs.rs/actix-web/4.0.0-beta.19) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-rc.1)](https://docs.rs/actix-web/4.0.0-rc.1)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.19/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.19) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-rc.1/status.svg)](https://deps.rs/crate/actix-web/4.0.0-rc.1)
<br /> <br />
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
@ -21,12 +21,13 @@
## Features ## Features
- Supports *HTTP/1.x* and *HTTP/2* - Supports _HTTP/1.x_ and _HTTP/2_
- Streaming and pipelining - 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 - Keep-alive and slow requests handling
- Client/server [WebSockets](https://actix.rs/docs/websockets/) support - Client/server [WebSockets](https://actix.rs/docs/websockets/) support
- Transparent content compression/decompression (br, gzip, deflate, zstd) - Transparent content compression/decompression (br, gzip, deflate, zstd)
- Powerful [request routing](https://actix.rs/docs/url-dispatch/)
- Multipart streams - Multipart streams
- Static assets - Static assets
- SSL support using OpenSSL or Rustls - SSL support using OpenSSL or Rustls
@ -47,7 +48,7 @@ Dependencies:
```toml ```toml
[dependencies] [dependencies]
actix-web = "3" actix-web = "4.0.0-rc.1"
``` ```
Code: Code:
@ -56,14 +57,15 @@ Code:
use actix_web::{get, web, App, HttpServer, Responder}; use actix_web::{get, web, App, HttpServer, Responder};
#[get("/{id}/{name}/index.html")] #[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) format!("Hello {}! id:{}", name, id)
} }
#[actix_web::main] #[actix_web::main] // or #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(index)) HttpServer::new(|| App::new().service(index))
.bind("127.0.0.1:8080")? .bind(("127.0.0.1", 8080))?
.run() .run()
.await .await
} }
@ -84,24 +86,18 @@ async fn main() -> std::io::Result<()> {
- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/) - [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/) - [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
You may consider checking out You may consider checking out [this directory](https://github.com/actix/examples/tree/master/) for more examples.
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
## Benchmarks ## Benchmarks
One of the fastest web frameworks available according to the One of the fastest web frameworks available according to the [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
## License ## 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 - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0])
[http://www.apache.org/licenses/LICENSE-2.0]) - MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT])
- MIT license ([LICENSE-MIT](LICENSE-MIT) or
[http://opensource.org/licenses/MIT])
at your option.
## Code of Conduct ## Code of Conduct

View File

@ -3,6 +3,20 @@
## Unreleased - 2021-xx-xx ## 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`.
## 0.6.0-beta.14 - 2022-01-14
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
[#2583]: https://github.com/actix/actix-web/pull/2583
## 0.6.0-beta.13 - 2022-01-04 ## 0.6.0-beta.13 - 2022-01-04
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398] - The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398] - The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.0-beta.13" version = "0.6.0-beta.16"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@ -22,10 +22,10 @@ path = "src/lib.rs"
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
[dependencies] [dependencies]
actix-http = "3.0.0-beta.18" actix-http = "3.0.0-rc.1"
actix-service = "2" actix-service = "2"
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4.0.0-beta.19", default-features = false } actix-web = { version = "4.0.0-rc.1", default-features = false }
askama_escape = "0.10" askama_escape = "0.10"
bitflags = "1" bitflags = "1"
@ -39,10 +39,10 @@ mime_guess = "2.0.1"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2.7" pin-project-lite = "0.2.7"
tokio-uring = { version = "0.1", optional = true } tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.11" actix-test = "0.1.0-beta.12"
actix-web = "4.0.0-beta.19" actix-web = "4.0.0-rc.1"
tempfile = "3.2" tempfile = "3.2"

View File

@ -3,11 +3,11 @@
> Static file serving for Actix Web > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.13)](https://docs.rs/actix-files/0.6.0-beta.13) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.16)](https://docs.rs/actix-files/0.6.0-beta.16)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.13/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.13) [![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.16/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.16)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -10,6 +10,9 @@ use actix_web::{error::Error, web::Bytes};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
#[cfg(feature = "experimental-io-uring")]
use bytes::BytesMut;
use super::named::File; use super::named::File;
pin_project! { pin_project! {
@ -214,64 +217,3 @@ where
} }
} }
} }
#[cfg(feature = "experimental-io-uring")]
use bytes_mut::BytesMut;
// TODO: remove new type and use bytes::BytesMut directly
#[doc(hidden)]
#[cfg(feature = "experimental-io-uring")]
mod bytes_mut {
use std::ops::{Deref, DerefMut};
use tokio_uring::buf::{IoBuf, IoBufMut};
#[derive(Debug)]
pub struct BytesMut(bytes::BytesMut);
impl BytesMut {
pub(super) fn new() -> Self {
Self(bytes::BytesMut::new())
}
}
impl Deref for BytesMut {
type Target = bytes::BytesMut;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BytesMut {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
unsafe impl IoBuf for BytesMut {
fn stable_ptr(&self) -> *const u8 {
self.0.as_ptr()
}
fn bytes_init(&self) -> usize {
self.0.len()
}
fn bytes_total(&self) -> usize {
self.0.capacity()
}
}
unsafe impl IoBufMut for BytesMut {
fn stable_mut_ptr(&mut self) -> *mut u8 {
self.0.as_mut_ptr()
}
unsafe fn set_init(&mut self, init_len: usize) {
if self.len() < init_len {
self.0.set_len(init_len);
}
}
}
}

View File

@ -37,7 +37,7 @@ use crate::{
/// .service(Files::new("/static", ".")); /// .service(Files::new("/static", "."));
/// ``` /// ```
pub struct Files { pub struct Files {
path: String, mount_path: String,
directory: PathBuf, directory: PathBuf,
index: Option<String>, index: Option<String>,
show_index: bool, show_index: bool,
@ -68,7 +68,7 @@ impl Clone for Files {
default: self.default.clone(), default: self.default.clone(),
renderer: self.renderer.clone(), renderer: self.renderer.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
path: self.path.clone(), mount_path: self.mount_path.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
path_filter: self.path_filter.clone(), path_filter: self.path_filter.clone(),
use_guards: self.use_guards.clone(), use_guards: self.use_guards.clone(),
@ -107,7 +107,7 @@ impl Files {
}; };
Files { Files {
path: mount_path.trim_end_matches('/').to_owned(), mount_path: mount_path.trim_end_matches('/').to_owned(),
directory: dir, directory: dir,
index: None, index: None,
show_index: false, show_index: false,
@ -342,9 +342,9 @@ impl HttpServiceFactory for Files {
} }
let rdef = if config.is_root() { let rdef = if config.is_root() {
ResourceDef::root_prefix(&self.path) ResourceDef::root_prefix(&self.mount_path)
} else { } else {
ResourceDef::prefix(&self.path) ResourceDef::prefix(&self.mount_path)
}; };
config.register_service(rdef, guards, self, None) config.register_service(rdef, guards, self, None)

View File

@ -2,7 +2,7 @@
//! //!
//! Provides a non-blocking service for serving static files from disk. //! Provides a non-blocking service for serving static files from disk.
//! //!
//! # Example //! # Examples
//! ``` //! ```
//! use actix_web::App; //! use actix_web::App;
//! use actix_files::Files; //! use actix_files::Files;
@ -67,8 +67,8 @@ mod tests {
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use actix_service::ServiceFactory;
use actix_web::{ use actix_web::{
dev::ServiceFactory,
guard, guard,
http::{ http::{
header::{self, ContentDisposition, DispositionParam, DispositionType}, header::{self, ContentDisposition, DispositionParam, DispositionType},
@ -106,7 +106,7 @@ mod tests {
let req = TestRequest::default() let req = TestRequest::default()
.insert_header((header::IF_MODIFIED_SINCE, since)) .insert_header((header::IF_MODIFIED_SINCE, since))
.to_http_request(); .to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
} }
@ -118,7 +118,7 @@ mod tests {
let req = TestRequest::default() let req = TestRequest::default()
.insert_header((header::IF_MODIFIED_SINCE, since)) .insert_header((header::IF_MODIFIED_SINCE, since))
.to_http_request(); .to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); 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_NONE_MATCH, "miss_etag"))
.insert_header((header::IF_MODIFIED_SINCE, since)) .insert_header((header::IF_MODIFIED_SINCE, since))
.to_http_request(); .to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
} }
@ -143,7 +143,7 @@ mod tests {
let req = TestRequest::default() let req = TestRequest::default()
.insert_header((header::IF_UNMODIFIED_SINCE, since)) .insert_header((header::IF_UNMODIFIED_SINCE, since))
.to_http_request(); .to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
@ -155,7 +155,7 @@ mod tests {
let req = TestRequest::default() let req = TestRequest::default()
.insert_header((header::IF_UNMODIFIED_SINCE, since)) .insert_header((header::IF_UNMODIFIED_SINCE, since))
.to_http_request(); .to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED); assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
} }
@ -172,7 +172,7 @@ mod tests {
} }
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/x-toml" "text/x-toml"
@ -196,7 +196,7 @@ mod tests {
} }
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"Cargo.toml\"" "inline; filename=\"Cargo.toml\""
@ -207,7 +207,7 @@ mod tests {
.unwrap() .unwrap()
.disable_content_disposition(); .disable_content_disposition();
let req = TestRequest::default().to_http_request(); 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()); assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none());
} }
@ -235,7 +235,7 @@ mod tests {
} }
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/x-toml" "text/x-toml"
@ -261,7 +261,7 @@ mod tests {
} }
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/xml" "text/xml"
@ -284,7 +284,7 @@ mod tests {
} }
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"image/png" "image/png"
@ -300,10 +300,10 @@ mod tests {
let file = NamedFile::open_async("tests/test.js").await.unwrap(); let file = NamedFile::open_async("tests/test.js").await.unwrap();
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/javascript" "application/javascript; charset=utf-8"
); );
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
@ -330,7 +330,7 @@ mod tests {
} }
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"image/png" "image/png"
@ -353,7 +353,7 @@ mod tests {
} }
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/octet-stream" "application/octet-stream"
@ -379,7 +379,7 @@ mod tests {
} }
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/x-toml" "text/x-toml"
@ -633,7 +633,7 @@ mod tests {
async fn test_named_file_allowed_method() { async fn test_named_file_allowed_method() {
let req = TestRequest::default().method(Method::GET).to_http_request(); let req = TestRequest::default().method(Method::GET).to_http_request();
let file = NamedFile::open_async("Cargo.toml").await.unwrap(); 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); assert_eq!(resp.status(), StatusCode::OK);
} }

View File

@ -1,15 +1,16 @@
use std::{ use std::{
fmt,
fs::Metadata, fs::Metadata,
io, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use actix_service::{Service, ServiceFactory};
use actix_web::{ use actix_web::{
body::{self, BoxBody, SizedStream}, body::{self, BoxBody, SizedStream},
dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, dev::{
self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
ServiceRequest, ServiceResponse,
},
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, ContentEncoding, DispositionParam, self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
@ -37,7 +38,7 @@ bitflags! {
impl Default for Flags { impl Default for Flags {
fn default() -> Self { fn default() -> Self {
Flags::from_bits_truncate(0b0000_0111) Flags::from_bits_truncate(0b0000_1111)
} }
} }
@ -65,12 +66,12 @@ impl Default for Flags {
/// NamedFile::open_async("./static/index.html").await /// NamedFile::open_async("./static/index.html").await
/// } /// }
/// ``` /// ```
#[derive(Deref, DerefMut)] #[derive(Debug, Deref, DerefMut)]
pub struct NamedFile { pub struct NamedFile {
path: PathBuf,
#[deref] #[deref]
#[deref_mut] #[deref_mut]
file: File, file: File,
path: PathBuf,
modified: Option<SystemTime>, modified: Option<SystemTime>,
pub(crate) md: Metadata, pub(crate) md: Metadata,
pub(crate) flags: Flags, pub(crate) flags: Flags,
@ -80,32 +81,6 @@ pub struct NamedFile {
pub(crate) encoding: Option<ContentEncoding>, pub(crate) encoding: Option<ContentEncoding>,
} }
impl fmt::Debug for NamedFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NamedFile")
.field("path", &self.path)
.field(
"file",
#[cfg(feature = "experimental-io-uring")]
{
&"tokio_uring::File"
},
#[cfg(not(feature = "experimental-io-uring"))]
{
&self.file
},
)
.field("modified", &self.modified)
.field("md", &self.md)
.field("flags", &self.flags)
.field("status_code", &self.status_code)
.field("content_type", &self.content_type)
.field("content_disposition", &self.content_disposition)
.field("encoding", &self.encoding)
.finish()
}
}
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
pub(crate) use std::fs::File; pub(crate) use std::fs::File;
#[cfg(feature = "experimental-io-uring")] #[cfg(feature = "experimental-io-uring")]
@ -234,6 +209,7 @@ impl NamedFile {
Self::from_file(file, path) Self::from_file(file, path)
} }
#[allow(rustdoc::broken_intra_doc_links)]
/// Attempts to open a file asynchronously in read-only mode. /// Attempts to open a file asynchronously in read-only mode.
/// ///
/// When the `experimental-io-uring` crate feature is enabled, this will be async. /// When the `experimental-io-uring` crate feature is enabled, this will be async.
@ -323,9 +299,11 @@ impl NamedFile {
self 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] #[inline]
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
self.encoding = Some(enc); self.encoding = Some(enc);
@ -627,7 +605,7 @@ impl Service<ServiceRequest> for NamedFileService {
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); dev::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();

View File

@ -59,6 +59,8 @@ impl PathBufWrap {
continue; continue;
} else if cfg!(windows) && segment.contains('\\') { } else if cfg!(windows) && segment.contains('\\') {
return Err(UriSegmentError::BadChar('\\')); return Err(UriSegmentError::BadChar('\\'));
} else if cfg!(windows) && segment.contains(':') {
return Err(UriSegmentError::BadChar(':'));
} else { } else {
buf.push(segment) buf.push(segment)
} }
@ -66,7 +68,11 @@ impl PathBufWrap {
// make sure we agree with stdlib parser // make sure we agree with stdlib parser
for (i, component) in buf.components().enumerate() { 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); assert!(i < segment_count);
} }
@ -85,7 +91,7 @@ impl FromRequest for PathBufWrap {
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(req.match_info().path().parse()) ready(req.match_info().unprocessed().parse())
} }
} }
@ -159,4 +165,26 @@ mod tests {
PathBuf::from_iter(vec!["etc/passwd"]) 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"])
);
}
} }

View File

@ -2,7 +2,7 @@ use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
use actix_web::{ use actix_web::{
body::BoxBody, body::BoxBody,
dev::{Service, ServiceRequest, ServiceResponse}, dev::{self, Service, ServiceRequest, ServiceResponse},
error::Error, error::Error,
guard::Guard, guard::Guard,
http::{header, Method}, http::{header, Method},
@ -98,7 +98,7 @@ impl Service<ServiceRequest> for FilesService {
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); dev::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let is_method_valid = if let Some(guard) = &self.guards { let is_method_valid = if let Some(guard) = &self.guards {
@ -120,14 +120,16 @@ impl Service<ServiceRequest> for FilesService {
)); ));
} }
let real_path = let path_on_disk = match PathBufWrap::parse_path(
match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) { req.match_info().unprocessed(),
this.hidden_files,
) {
Ok(item) => item, Ok(item) => item,
Err(err) => return Ok(req.error_response(err)), Err(err) => return Ok(req.error_response(err)),
}; };
if let Some(filter) = &this.path_filter { if let Some(filter) = &this.path_filter {
if !filter(real_path.as_ref(), req.head()) { if !filter(path_on_disk.as_ref(), req.head()) {
if let Some(ref default) = this.default { if let Some(ref default) = this.default {
return default.call(req).await; return default.call(req).await;
} else { } else {
@ -137,7 +139,7 @@ impl Service<ServiceRequest> for FilesService {
} }
// full file path // full file path
let path = this.directory.join(&real_path); let path = this.directory.join(&path_on_disk);
if let Err(err) = path.canonicalize() { if let Err(err) = path.canonicalize() {
return this.handle_err(err, req).await; return this.handle_err(err, req).await;
} }
@ -166,7 +168,7 @@ impl Service<ServiceRequest> for FilesService {
} }
} }
None if this.show_index => Ok(this.show_index(req, path)), None if this.show_index => Ok(this.show_index(req, path)),
_ => Ok(ServiceResponse::from_err( None => Ok(ServiceResponse::from_err(
FilesError::IsDirectory, FilesError::IsDirectory,
req.into_parts().0, req.into_parts().0,
)), )),

View File

@ -19,12 +19,12 @@ async fn test_utf8_file_contents() {
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(header::CONTENT_TYPE), res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain")), Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
); );
// prefer UTF-8 encoding // disable UTF-8 attribute
let srv = let srv =
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true))) test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false)))
.await; .await;
let req = TestRequest::with_uri("/utf8.txt").to_request(); let req = TestRequest::with_uri("/utf8.txt").to_request();
@ -33,6 +33,6 @@ async fn test_utf8_file_contents() {
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(header::CONTENT_TYPE), res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain; charset=utf-8")), Some(&HeaderValue::from_static("text/plain")),
); );
} }

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## 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 ## 3.0.0-beta.11 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.11" version = "3.0.0-beta.12"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@ -34,8 +34,8 @@ actix-codec = "0.4.1"
actix-tls = "3.0.0" actix-tls = "3.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2.0.0-rc.2" actix-server = "2"
awc = { version = "3.0.0-beta.18", default-features = false } awc = { version = "3.0.0-beta.20", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.19", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-rc.1", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.18" actix-http = "3.0.0-rc.1"

View File

@ -3,11 +3,11 @@
> Various helpers for Actix applications to use during testing. > Various helpers for Actix applications to use during testing.
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.11)](https://docs.rs/actix-http-test/3.0.0-beta.11) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.12)](https://docs.rs/actix-http-test/3.0.0-beta.12)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br> <br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.11) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.12/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.12)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -3,6 +3,55 @@
## Unreleased - 2021-xx-xx ## 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]
- `ResponseHead` now implements `Clone`. [#2585]
### Changed
- Brotli (de)compression support is now provided by the `brotli` crate. [#2538]
### Removed
- `ResponseHead::extensions[_mut]()`. [#2585]
- `ResponseBuilder::extensions[_mut]()`. [#2585]
[#2538]: https://github.com/actix/actix-web/pull/2538
[#2585]: https://github.com/actix/actix-web/pull/2585
[#2587]: https://github.com/actix/actix-web/pull/2587
## 3.0.0-beta.18 - 2022-01-04 ## 3.0.0-beta.18 - 2022-01-04
### Added ### Added
- `impl Eq` for `header::ContentEncoding`. [#2501] - `impl Eq` for `header::ContentEncoding`. [#2501]
@ -15,8 +64,8 @@
- `Quality::MIN` is now the smallest non-zero value. [#2501] - `Quality::MIN` is now the smallest non-zero value. [#2501]
- `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501] - `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501]
- Rename `ContentEncoding::{Br => Brotli}`. [#2501] - Rename `ContentEncoding::{Br => Brotli}`. [#2501]
- Minimum supported Rust version (MSRV) is now 1.54.
- Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565] - Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565]
- Minimum supported Rust version (MSRV) is now 1.54.
### Fixed ### Fixed
- `ContentEncoding::Identity` can now be parsed from a string. [#2501] - `ContentEncoding::Identity` can now be parsed from a string. [#2501]
@ -399,6 +448,13 @@
[#1878]: https://github.com/actix/actix-web/pull/1878 [#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 ## 2.2.1 - 2021-08-09
### Fixed ### Fixed
- Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) - Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)

View File

@ -1,7 +1,10 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.18" version = "3.0.0-rc.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -26,68 +29,85 @@ path = "src/lib.rs"
[features] [features]
default = [] 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"] openssl = ["actix-tls/accept", "actix-tls/openssl"]
# rustls support # TLS via Rustls
rustls = ["actix-tls/accept", "actix-tls/rustls"] rustls = ["actix-tls/accept", "actix-tls/rustls"]
# enable compression support # Compression codecs
compress-brotli = ["brotli2", "__compress"] compress-brotli = ["__compress", "brotli"]
compress-gzip = ["flate2", "__compress"] compress-gzip = ["__compress", "flate2"]
compress-zstd = ["zstd", "__compress"] compress-zstd = ["__compress", "zstd"]
# Internal (PRIVATE!) features used to aid testing and cheking feature status. # 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 = [] __compress = []
[dependencies] [dependencies]
actix-service = "2.0.0" actix-service = "2"
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-utils = "3.0.0" actix-utils = "3"
actix-rt = { version = "2.2", default-features = false } actix-rt = { version = "2.2", default-features = false }
ahash = "0.7" ahash = "0.7"
base64 = "0.13"
bitflags = "1.2" bitflags = "1.2"
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
h2 = "0.3.9"
http = "0.2.5" http = "0.2.5"
httparse = "1.5.1" httparse = "1.5.1"
httpdate = "1.0.1" httpdate = "1.0.1"
itoa = "1" itoa = "1"
language-tags = "0.3" language-tags = "0.3"
local-channel = "0.1"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2" pin-project-lite = "0.2"
rand = "0.8"
sha-1 = "0.10"
smallvec = "1.6.1" 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 } actix-tls = { version = "3.0.0", default-features = false, optional = true }
# compression # compress-*
brotli2 = { version="0.3.2", optional = true } brotli = { version = "3.3.3", optional = true }
flate2 = { version = "1.0.13", optional = true } flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.9", optional = true } zstd = { version = "0.9", optional = true }
[dev-dependencies] [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.0.0-rc.2" actix-server = "2"
actix-tls = { version = "3.0.0", features = ["openssl"] } actix-tls = { version = "3.0.0", features = ["openssl"] }
actix-web = "4.0.0-beta.19" actix-web = "4.0.0-rc.1"
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
memchr = "2.4"
once_cell = "1.9"
rcgen = "0.8" rcgen = "0.8"
regex = "1.3" regex = "1.3"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"

View File

@ -3,11 +3,11 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.18)](https://docs.rs/actix-http/3.0.0-beta.18) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-rc.1)](https://docs.rs/actix-http/3.0.0-rc.1)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.18/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.18) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-rc.1/status.svg)](https://deps.rs/crate/actix-http/3.0.0-rc.1)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -42,32 +42,37 @@ mod _new {
if x < 10 { if x < 10 {
f.write_str("00")?; f.write_str("00")?;
// 0 is handled so it's not possible to have a trailing 0, we can just return // 0 is handled so it's not possible to have a trailing 0, we can just return
itoa::fmt(f, x) itoa_fmt(f, x)
} else if x < 100 { } else if x < 100 {
f.write_str("0")?; f.write_str("0")?;
if x % 10 == 0 { if x % 10 == 0 {
// trailing 0, divide by 10 and write // trailing 0, divide by 10 and write
itoa::fmt(f, x / 10) itoa_fmt(f, x / 10)
} else { } else {
itoa::fmt(f, x) itoa_fmt(f, x)
} }
} else { } else {
// x is in range 101999 // x is in range 101999
if x % 100 == 0 { if x % 100 == 0 {
// two trailing 0s, divide by 100 and write // two trailing 0s, divide by 100 and write
itoa::fmt(f, x / 100) itoa_fmt(f, x / 100)
} else if x % 10 == 0 { } else if x % 10 == 0 {
// one trailing 0, divide by 10 and write // one trailing 0, divide by 10 and write
itoa::fmt(f, x / 10) itoa_fmt(f, x / 10)
} else { } else {
itoa::fmt(f, x) itoa_fmt(f, x)
} }
} }
} }
} }
} }
} }
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
let mut buf = itoa::Buffer::new();
wr.write_str(buf.format(value))
}
} }
mod _naive { mod _naive {

View 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
}

View File

@ -1,4 +1,4 @@
use std::io; use std::{io, time::Duration};
use actix_http::{Error, HttpService, Request, Response, StatusCode}; use actix_http::{Error, HttpService, Request, Response, StatusCode};
use actix_server::Server; use actix_server::Server;
@ -13,8 +13,9 @@ async fn main() -> io::Result<()> {
Server::build() Server::build()
.bind("echo", ("127.0.0.1", 8080), || { .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.client_timeout(1000) .client_request_timeout(Duration::from_secs(1))
.client_disconnect(1000) .client_disconnect_timeout(Duration::from_secs(1))
// handles HTTP/1.1 and HTTP/2
.finish(|mut req: Request| async move { .finish(|mut req: Request| async move {
let mut body = BytesMut::new(); let mut body = BytesMut::new();
while let Some(item) = req.payload().next().await { while let Some(item) = req.payload().next().await {
@ -23,12 +24,13 @@ async fn main() -> io::Result<()> {
log::info!("request body: {:?}", body); log::info!("request body: {:?}", body);
Ok::<_, Error>( let res = Response::build(StatusCode::OK)
Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!"))) .insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body), .body(body);
)
Ok::<_, Error>(res)
}) })
// No TLS
.tcp() .tcp()
})? })?
.run() .run()

View File

@ -1,32 +1,34 @@
use std::io; use std::io;
use actix_http::{ use actix_http::{
body::MessageBody, header::HeaderValue, Error, HttpService, Request, Response, StatusCode, body::{BodyStream, MessageBody},
header, Error, HttpMessage, HttpService, Request, Response, StatusCode,
}; };
use actix_server::Server;
use bytes::BytesMut;
use futures_util::StreamExt as _;
async fn handle_request(mut req: Request) -> Result<Response<impl MessageBody>, Error> { async fn handle_request(mut req: Request) -> Result<Response<impl MessageBody>, Error> {
let mut body = BytesMut::new(); let mut res = Response::build(StatusCode::OK);
while let Some(item) = req.payload().next().await {
body.extend_from_slice(&item?) if let Some(ct) = req.headers().get(header::CONTENT_TYPE) {
res.insert_header((header::CONTENT_TYPE, ct));
} }
log::info!("request body: {:?}", body); // echo request payload stream as (chunked) response body
let res = res.message_body(BodyStream::new(req.payload().take()))?;
Ok(Response::build(StatusCode::OK) Ok(res)
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body))
} }
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build() actix_server::Server::build()
.bind("echo", ("127.0.0.1", 8080), || { .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build().finish(handle_request).tcp() HttpService::build()
// handles HTTP/1.1 only
.h1(handle_request)
// No TLS
.tcp()
})? })?
.run() .run()
.await .await

View 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
}

View File

@ -1,4 +1,4 @@
use std::{convert::Infallible, io}; use std::{convert::Infallible, io, time::Duration};
use actix_http::{ use actix_http::{
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode, header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
@ -12,8 +12,8 @@ async fn main() -> io::Result<()> {
Server::build() Server::build()
.bind("hello-world", ("127.0.0.1", 8080), || { .bind("hello-world", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.client_timeout(1000) .client_request_timeout(Duration::from_secs(1))
.client_disconnect(1000) .client_disconnect_timeout(Duration::from_secs(1))
.on_connect_ext(|_, ext| { .on_connect_ext(|_, ext| {
ext.insert(42u32); ext.insert(42u32);
}) })

View File

@ -31,7 +31,7 @@ impl fmt::Debug for BoxBodyInner {
} }
impl BoxBody { 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 /// If the body type to wrap is unknown or generic it is better to use [`MessageBody::boxed`] to
/// avoid double boxing. /// avoid double boxing.

View File

@ -14,8 +14,44 @@ use pin_project_lite::pin_project;
use super::{BodySize, BoxBody}; use super::{BodySize, BoxBody};
/// An interface types that can converted to bytes and used as response bodies. /// An interface for types that can be used as a response body.
// TODO: examples ///
/// 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 { pub trait MessageBody {
/// The type of error that will be returned if streaming body fails. /// The type of error that will be returned if streaming body fails.
/// ///
@ -29,7 +65,22 @@ pub trait MessageBody {
fn size(&self) -> BodySize; fn size(&self) -> BodySize;
/// Attempt to pull out the next chunk of body bytes. /// 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, Rusts usual rules apply: calls must never cause UB, regardless of its state.
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
@ -37,7 +88,7 @@ pub trait MessageBody {
/// Try to convert into the complete chunk of body bytes. /// 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. /// optimizations where `poll_next` calls can be avoided.
/// ///
/// Body types with [`BodySize::None`] are allowed to return empty `Bytes`. Although, if calling /// Body types with [`BodySize::None`] are allowed to return empty `Bytes`. Although, if calling
@ -54,7 +105,11 @@ pub trait MessageBody {
Err(self) 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] #[inline]
fn boxed(self) -> BoxBody fn boxed(self) -> BoxBody
where where

View File

@ -1,4 +1,9 @@
//! Traits and structures to aid consuming and writing HTTP payloads. //! 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 body_stream;
mod boxed; mod boxed;

View File

@ -10,9 +10,12 @@ use super::{BodySize, MessageBody};
/// Body type for responses that forbid payloads. /// 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 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)] #[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive] #[non_exhaustive]
pub struct None; pub struct None;

View File

@ -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_codec::Framed;
use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use actix_service::{IntoServiceFactory, Service, ServiceFactory};
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
config::{KeepAlive, ServiceConfig},
h1::{self, ExpectHandler, H1Service, UpgradeHandler}, h1::{self, ExpectHandler, H1Service, UpgradeHandler},
h2::H2Service,
service::HttpService, 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 /// This type can construct an instance of [`HttpService`] through a builder-like pattern.
/// builder-like pattern.
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> { pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_timeout: u64, client_request_timeout: Duration,
client_disconnect: u64, client_disconnect_timeout: Duration,
secure: bool, secure: bool,
local_addr: Option<net::SocketAddr>, local_addr: Option<net::SocketAddr>,
expect: X, expect: X,
@ -28,22 +25,23 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
_phantom: PhantomData<S>, _phantom: PhantomData<S>,
} }
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler> impl<T, S> Default for HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Response<BoxBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
{ {
/// Create instance of `ServiceConfigBuilder` fn default() -> Self {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: KeepAlive::Timeout(5), // ServiceConfig parts (make sure defaults match)
client_timeout: 5000, keep_alive: KeepAlive::default(),
client_disconnect: 0, client_request_timeout: Duration::from_secs(5),
client_disconnect_timeout: Duration::ZERO,
secure: false, secure: false,
local_addr: None, local_addr: None,
// dispatcher parts
expect: ExpectHandler, expect: ExpectHandler,
upgrade: None, upgrade: None,
on_connect_ext: None, on_connect_ext: None,
@ -65,9 +63,11 @@ where
U::Error: fmt::Display, U::Error: fmt::Display,
U::InitError: fmt::Debug, 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 { pub fn keep_alive<W: Into<KeepAlive>>(mut self, val: W) -> Self {
self.keep_alive = val.into(); self.keep_alive = val.into();
self self
@ -85,33 +85,45 @@ where
self 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 /// Defines a timeout for reading client request header. If the client does not transmit the
/// the entire set headers within this time, the request is terminated with /// request head within this duration, the connection is terminated with a `408 Request Timeout`
/// the 408 (Request Time-out) error. /// 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. /// By default, the client timeout is 5 seconds.
pub fn client_timeout(mut self, val: u64) -> Self { pub fn client_request_timeout(mut self, dur: Duration) -> Self {
self.client_timeout = val; self.client_request_timeout = dur;
self 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 /// 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. /// 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. /// By default, the disconnect timeout is disabled.
pub fn client_disconnect(mut self, val: u64) -> Self { pub fn client_disconnect_timeout(mut self, dur: Duration) -> Self {
self.client_disconnect = val; self.client_disconnect_timeout = dur;
self 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. /// Provide service for `EXPECT: 100-Continue` support.
/// ///
/// Service get called with request that contains `EXPECT` header. /// Service get called with request that contains `EXPECT` header.
@ -126,8 +138,8 @@ where
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
client_timeout: self.client_timeout, client_request_timeout: self.client_request_timeout,
client_disconnect: self.client_disconnect, client_disconnect_timeout: self.client_disconnect_timeout,
secure: self.secure, secure: self.secure,
local_addr: self.local_addr, local_addr: self.local_addr,
expect: expect.into_factory(), expect: expect.into_factory(),
@ -150,8 +162,8 @@ where
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
client_timeout: self.client_timeout, client_request_timeout: self.client_request_timeout,
client_disconnect: self.client_disconnect, client_disconnect_timeout: self.client_disconnect_timeout,
secure: self.secure, secure: self.secure,
local_addr: self.local_addr, local_addr: self.local_addr,
expect: self.expect, expect: self.expect,
@ -185,8 +197,8 @@ where
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
self.client_timeout, self.client_request_timeout,
self.client_disconnect, self.client_disconnect_timeout,
self.secure, self.secure,
self.local_addr, self.local_addr,
); );
@ -198,7 +210,8 @@ where
} }
/// Finish service configuration and create a HTTP service for HTTP/2 protocol. /// Finish service configuration and create a HTTP service for HTTP/2 protocol.
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B> #[cfg(feature = "http2")]
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
where where
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Response<BoxBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
@ -209,13 +222,14 @@ where
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
self.client_timeout, self.client_request_timeout,
self.client_disconnect, self.client_disconnect_timeout,
self.secure, self.secure,
self.local_addr, 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. /// Finish service configuration and create `HttpService` instance.
@ -230,8 +244,8 @@ where
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
self.client_timeout, self.client_request_timeout,
self.client_disconnect, self.client_disconnect_timeout,
self.secure, self.secure,
self.local_addr, self.local_addr,
); );

View File

@ -1,71 +1,36 @@
use std::{ use std::{
cell::Cell,
fmt::{self, Write},
net, net,
rc::Rc, rc::Rc,
time::{Duration, SystemTime}, time::{Duration, Instant},
}; };
use actix_rt::{
task::JoinHandle,
time::{interval, sleep_until, Instant, Sleep},
};
use bytes::BytesMut; use bytes::BytesMut;
/// "Sun, 06 Nov 1994 08:49:37 GMT".len() use crate::{date::DateService, KeepAlive};
pub(crate) const DATE_VALUE_LENGTH: usize = 29;
#[derive(Debug, PartialEq, Clone, Copy)] /// HTTP service configuration.
/// Server keep-alive setting #[derive(Debug, Clone)]
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
pub struct ServiceConfig(Rc<Inner>); pub struct ServiceConfig(Rc<Inner>);
#[derive(Debug)]
struct Inner { struct Inner {
keep_alive: Option<Duration>, keep_alive: KeepAlive,
client_timeout: u64, client_request_timeout: Duration,
client_disconnect: u64, client_disconnect_timeout: Duration,
ka_enabled: bool,
secure: bool, secure: bool,
local_addr: Option<std::net::SocketAddr>, local_addr: Option<std::net::SocketAddr>,
date_service: DateService, date_service: DateService,
} }
impl Clone for ServiceConfig {
fn clone(&self) -> Self {
ServiceConfig(self.0.clone())
}
}
impl Default for ServiceConfig { impl Default for ServiceConfig {
fn default() -> Self { 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` /// Create instance of `ServiceConfig`
pub fn new( pub fn new(
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_timeout: u64, client_request_timeout: Duration,
client_disconnect: u64, client_disconnect_timeout: Duration,
secure: bool, secure: bool,
local_addr: Option<net::SocketAddr>, local_addr: Option<net::SocketAddr>,
) -> ServiceConfig { ) -> 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 { ServiceConfig(Rc::new(Inner {
keep_alive, keep_alive: keep_alive.normalize(),
ka_enabled, client_request_timeout,
client_timeout, client_disconnect_timeout,
client_disconnect,
secure, secure,
local_addr, local_addr,
date_service: DateService::new(), date_service: DateService::new(),
})) }))
} }
/// Returns true if connection is secure (HTTPS) /// Returns `true` if connection is secure (i.e., using TLS / HTTPS).
#[inline] #[inline]
pub fn secure(&self) -> bool { pub fn secure(&self) -> bool {
self.0.secure self.0.secure
@ -114,235 +67,92 @@ impl ServiceConfig {
self.0.local_addr self.0.local_addr
} }
/// Keep alive duration if configured. /// Connection keep-alive setting.
#[inline] #[inline]
pub fn keep_alive(&self) -> Option<Duration> { pub fn keep_alive(&self) -> KeepAlive {
self.0.keep_alive self.0.keep_alive
} }
/// Return state of connection keep-alive functionality /// Creates a time object representing the deadline for this connection's keep-alive period, if
#[inline] /// enabled.
pub fn keep_alive_enabled(&self) -> bool { ///
self.0.ka_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() {
/// Client timeout for first request. KeepAlive::Timeout(dur) => Some(self.now() + dur),
#[inline] KeepAlive::Os => None,
pub fn client_timer(&self) -> Option<Sleep> { KeepAlive::Disabled => None,
let delay_time = self.0.client_timeout;
if delay_time != 0 {
Some(sleep_until(self.now() + Duration::from_millis(delay_time)))
} else {
None
} }
} }
/// Client timeout for first request. /// Creates a time object representing the deadline for the client to finish sending the head of
pub fn client_timer_expire(&self) -> Option<Instant> { /// its first request.
let delay = self.0.client_timeout; ///
if delay != 0 { /// Returns `None` if this `ServiceConfig was` constructed with `client_request_timeout: 0`.
Some(self.now() + Duration::from_millis(delay)) pub fn client_request_deadline(&self) -> Option<Instant> {
} else { let timeout = self.0.client_request_timeout;
None (timeout != Duration::ZERO).then(|| self.now() + timeout)
}
} }
/// Client disconnect timer /// Creates a time object representing the deadline for the client to disconnect.
pub fn client_disconnect_timer(&self) -> Option<Instant> { pub fn client_disconnect_deadline(&self) -> Option<Instant> {
let delay = self.0.client_disconnect; let timeout = self.0.client_disconnect_timeout;
if delay != 0 { (timeout != Duration::ZERO).then(|| self.now() + timeout)
Some(self.now() + Duration::from_millis(delay))
} else {
None
}
} }
/// 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 { pub(crate) fn now(&self) -> Instant {
self.0.date_service.now() self.0.date_service.now()
} }
#[doc(hidden)] pub(crate) fn write_date_header(&self, dst: &mut BytesMut, camel_case: bool) {
pub fn set_date(&self, dst: &mut BytesMut) {
let mut buf: [u8; 39] = [0; 39]; let mut buf: [u8; 39] = [0; 39];
buf[..6].copy_from_slice(b"date: ");
buf[..6].copy_from_slice(if camel_case { b"Date: " } else { b"date: " });
self.0 self.0
.date_service .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"); buf[35..].copy_from_slice(b"\r\n\r\n");
dst.extend_from_slice(&buf); 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 self.0
.date_service .date_service
.set_date(|date| dst.extend_from_slice(&date.bytes)); .with_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(&current);
// 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;
}
});
}
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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] #[actix_rt::test]
async fn test_date_service_update() { 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; yield_now().await;
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf1); settings.write_date_header(&mut buf1, false);
let now1 = settings.now(); 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; yield_now().await;
let now2 = settings.now(); let now2 = settings.now();
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf2); settings.write_date_header(&mut buf2, false);
assert_ne!(now1, now2); assert_ne!(now1, now2);
@ -395,11 +205,27 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_date() { async fn test_date() {
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None); let settings = ServiceConfig::default();
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf1); settings.write_date_header(&mut buf1, false);
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf2); settings.write_date_header(&mut buf2, false);
assert_eq!(buf1, buf2); assert_eq!(buf1, buf2);
} }
#[actix_rt::test]
async fn test_date_camel_case() {
let settings = ServiceConfig::default();
let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
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.write_date_header(&mut buf, true);
assert!(memmem::find(&buf, b"Date:").is_some());
}
} }

92
actix-http/src/date.rs Normal file
View 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(&current);
// 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();
}
}

View File

@ -11,9 +11,6 @@ use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
#[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliDecoder;
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
use flate2::write::{GzDecoder, ZlibDecoder}; use flate2::write::{GzDecoder, ZlibDecoder};
@ -48,7 +45,7 @@ where
let decoder = match encoding { let decoder = match encoding {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new( ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
BrotliDecoder::new(Writer::new()), brotli::DecompressorWriter::new(Writer::new(), 8_096),
))), ))),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
@ -165,7 +162,7 @@ enum ContentDecoder {
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
Gzip(Box<GzDecoder<Writer>>), Gzip(Box<GzDecoder<Writer>>),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
Brotli(Box<BrotliDecoder<Writer>>), Brotli(Box<brotli::DecompressorWriter<Writer>>),
// We need explicit 'static lifetime here because ZstdDecoder need lifetime // We need explicit 'static lifetime here because ZstdDecoder need lifetime
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static` // argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]

View File

@ -14,9 +14,6 @@ use derive_more::Display;
use futures_core::ready; use futures_core::ready;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
#[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliEncoder;
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
use flate2::write::{GzEncoder, ZlibEncoder}; use flate2::write::{GzEncoder, ZlibEncoder};
@ -268,7 +265,7 @@ enum ContentEncoder {
Gzip(GzEncoder<Writer>), Gzip(GzEncoder<Writer>),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
Brotli(BrotliEncoder<Writer>), Brotli(Box<brotli::CompressorWriter<Writer>>),
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we // Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`. // use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
@ -292,9 +289,7 @@ impl ContentEncoder {
))), ))),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoding::Brotli => { ContentEncoding::Brotli => Some(ContentEncoder::Brotli(new_brotli_compressor())),
Some(ContentEncoder::Brotli(BrotliEncoder::new(Writer::new(), 3)))
}
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
ContentEncoding::Zstd => { ContentEncoding::Zstd => {
@ -326,8 +321,8 @@ impl ContentEncoder {
fn finish(self) -> Result<Bytes, io::Error> { fn finish(self) -> Result<Bytes, io::Error> {
match self { match self {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoder::Brotli(encoder) => match encoder.finish() { ContentEncoder::Brotli(mut encoder) => match encoder.flush() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(()) => Ok(encoder.into_inner().buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
@ -357,7 +352,7 @@ impl ContentEncoder {
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
trace!("Error decoding br encoding: {}", err); log::trace!("Error decoding br encoding: {}", err);
Err(err) Err(err)
} }
}, },
@ -366,7 +361,7 @@ impl ContentEncoder {
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
trace!("Error decoding gzip encoding: {}", err); log::trace!("Error decoding gzip encoding: {}", err);
Err(err) Err(err)
} }
}, },
@ -375,7 +370,7 @@ impl ContentEncoder {
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
trace!("Error decoding deflate encoding: {}", err); log::trace!("Error decoding deflate encoding: {}", err);
Err(err) Err(err)
} }
}, },
@ -384,7 +379,7 @@ impl ContentEncoder {
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
trace!("Error decoding ztsd encoding: {}", err); log::trace!("Error decoding ztsd encoding: {}", err);
Err(err) Err(err)
} }
}, },
@ -392,6 +387,16 @@ impl ContentEncoder {
} }
} }
#[cfg(feature = "compress-brotli")]
fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
Box::new(brotli::CompressorWriter::new(
Writer::new(),
32 * 1024, // 32 KiB buffer
3, // BROTLI_PARAM_QUALITY
22, // BROTLI_PARAM_LGWIN
))
}
#[derive(Debug, Display)] #[derive(Debug, Display)]
#[non_exhaustive] #[non_exhaustive]
pub enum EncoderError { pub enum EncoderError {

View File

@ -5,7 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use http::{uri::InvalidUri, StatusCode}; use http::{uri::InvalidUri, StatusCode};
use crate::{body::BoxBody, ws, Response}; use crate::{body::BoxBody, Response};
pub use http::Error as HttpError; pub use http::Error as HttpError;
@ -61,6 +61,7 @@ impl Error {
Self::new(Kind::Encoder) Self::new(Kind::Encoder)
} }
#[allow(unused)] // used with `ws` feature flag
pub(crate) fn new_ws() -> Self { pub(crate) fn new_ws() -> Self {
Self::new(Kind::Ws) Self::new(Kind::Ws)
} }
@ -139,14 +140,16 @@ impl From<HttpError> for Error {
} }
} }
impl From<ws::HandshakeError> for Error { #[cfg(feature = "ws")]
fn from(err: ws::HandshakeError) -> Self { impl From<crate::ws::HandshakeError> for Error {
fn from(err: crate::ws::HandshakeError) -> Self {
Self::new_ws().with_cause(err) Self::new_ws().with_cause(err)
} }
} }
impl From<ws::ProtocolError> for Error { #[cfg(feature = "ws")]
fn from(err: ws::ProtocolError) -> Self { impl From<crate::ws::ProtocolError> for Error {
fn from(err: crate::ws::ProtocolError) -> Self {
Self::new_ws().with_cause(err) Self::new_ws().with_cause(err)
} }
} }
@ -277,8 +280,9 @@ pub enum PayloadError {
UnknownLength, UnknownLength,
/// HTTP/2 payload error. /// HTTP/2 payload error.
#[cfg(feature = "http2")]
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http2Payload(h2::Error), Http2Payload(::h2::Error),
/// Generic I/O error. /// Generic I/O error.
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
@ -293,14 +297,16 @@ impl std::error::Error for PayloadError {
PayloadError::EncodingCorrupted => None, PayloadError::EncodingCorrupted => None,
PayloadError::Overflow => None, PayloadError::Overflow => None,
PayloadError::UnknownLength => None, PayloadError::UnknownLength => None,
#[cfg(feature = "http2")]
PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error), PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error),
PayloadError::Io(err) => Some(err as &dyn std::error::Error), PayloadError::Io(err) => Some(err as &dyn std::error::Error),
} }
} }
} }
impl From<h2::Error> for PayloadError { #[cfg(feature = "http2")]
fn from(err: h2::Error) -> Self { impl From<::h2::Error> for PayloadError {
fn from(err: ::h2::Error) -> Self {
PayloadError::Http2Payload(err) PayloadError::Http2Payload(err)
} }
} }
@ -356,6 +362,7 @@ pub enum DispatchError {
/// HTTP/2 error. /// HTTP/2 error.
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
#[cfg(feature = "http2")]
H2(h2::Error), H2(h2::Error),
/// The first request did not complete within the specified timeout. /// The first request did not complete within the specified timeout.
@ -379,7 +386,10 @@ impl StdError for DispatchError {
DispatchError::Body(err) => Some(&**err), DispatchError::Body(err) => Some(&**err),
DispatchError::Io(err) => Some(err), DispatchError::Io(err) => Some(err),
DispatchError::Parse(err) => Some(err), DispatchError::Parse(err) => Some(err),
#[cfg(feature = "http2")]
DispatchError::H2(err) => Some(err), DispatchError::H2(err) => Some(err),
_ => None, _ => None,
} }
} }
@ -387,6 +397,7 @@ impl StdError for DispatchError {
/// A set of error that can occur during parsing content type. /// A set of error that can occur during parsing content type.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[cfg_attr(test, derive(PartialEq))]
#[non_exhaustive] #[non_exhaustive]
pub enum ContentTypeError { pub enum ContentTypeError {
/// Can not parse content type /// Can not parse content type
@ -398,28 +409,14 @@ pub enum ContentTypeError {
UnknownEncoding, UnknownEncoding,
} }
#[cfg(test)]
mod content_type_test_impls {
use super::*;
impl std::cmp::PartialEq for ContentTypeError {
fn eq(&self, other: &Self) -> bool {
match self {
Self::ParseError => matches!(other, ContentTypeError::ParseError),
Self::UnknownEncoding => {
matches!(other, ContentTypeError::UnknownEncoding)
}
}
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use http::{Error as HttpError, StatusCode};
use std::io; use std::io;
use http::{Error as HttpError, StatusCode};
use super::*;
#[test] #[test]
fn test_into_response() { fn test_into_response() {
let resp: Response<BoxBody> = ParseError::Incomplete.into(); let resp: Response<BoxBody> = ParseError::Incomplete.into();

View File

@ -1,4 +1,4 @@
use std::io; use std::{fmt, io};
use actix_codec::{Decoder, Encoder}; use actix_codec::{Decoder, Encoder};
use bitflags::bitflags; use bitflags::bitflags;
@ -18,7 +18,7 @@ use crate::{
bitflags! { bitflags! {
struct Flags: u8 { struct Flags: u8 {
const HEAD = 0b0000_0001; const HEAD = 0b0000_0001;
const KEEPALIVE_ENABLED = 0b0000_1000; const KEEP_ALIVE_ENABLED = 0b0000_1000;
const STREAM = 0b0001_0000; const STREAM = 0b0001_0000;
} }
} }
@ -38,7 +38,7 @@ struct ClientCodecInner {
decoder: decoder::MessageDecoder<ResponseHead>, decoder: decoder::MessageDecoder<ResponseHead>,
payload: Option<PayloadDecoder>, payload: Option<PayloadDecoder>,
version: Version, version: Version,
ctype: ConnectionType, conn_type: ConnectionType,
// encoder part // encoder part
flags: Flags, 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 { impl ClientCodec {
/// Create HTTP/1 codec. /// Create HTTP/1 codec.
/// ///
/// `keepalive_enabled` how response `connection` header get generated. /// `keepalive_enabled` how response `connection` header get generated.
pub fn new(config: ServiceConfig) -> Self { pub fn new(config: ServiceConfig) -> Self {
let flags = if config.keep_alive_enabled() { let flags = if config.keep_alive().enabled() {
Flags::KEEPALIVE_ENABLED Flags::KEEP_ALIVE_ENABLED
} else { } else {
Flags::empty() Flags::empty()
}; };
ClientCodec { ClientCodec {
inner: ClientCodecInner { inner: ClientCodecInner {
config, config,
decoder: decoder::MessageDecoder::default(), decoder: decoder::MessageDecoder::default(),
payload: None, payload: None,
version: Version::HTTP_11, version: Version::HTTP_11,
ctype: ConnectionType::Close, conn_type: ConnectionType::Close,
flags, flags,
encoder: encoder::MessageEncoder::default(), encoder: encoder::MessageEncoder::default(),
@ -77,12 +86,12 @@ impl ClientCodec {
/// Check if request is upgrade /// Check if request is upgrade
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.inner.ctype == ConnectionType::Upgrade self.inner.conn_type == ConnectionType::Upgrade
} }
/// Check if last response is keep-alive /// Check if last response is keep-alive
pub fn keepalive(&self) -> bool { pub fn keep_alive(&self) -> bool {
self.inner.ctype == ConnectionType::KeepAlive self.inner.conn_type == ConnectionType::KeepAlive
} }
/// Check last request's message type /// Check last request's message type
@ -104,8 +113,8 @@ impl ClientCodec {
impl ClientPayloadCodec { impl ClientPayloadCodec {
/// Check if last response is keep-alive /// Check if last response is keep-alive
pub fn keepalive(&self) -> bool { pub fn keep_alive(&self) -> bool {
self.inner.ctype == ConnectionType::KeepAlive self.inner.conn_type == ConnectionType::KeepAlive
} }
/// Transform payload codec to a message codec /// 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"); debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
if let Some((req, payload)) = self.inner.decoder.decode(src)? { 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 // do not use peer's keep-alive
self.inner.ctype = if ctype == ConnectionType::KeepAlive { self.inner.conn_type = if conn_type == ConnectionType::KeepAlive {
self.inner.ctype self.inner.conn_type
} else { } else {
ctype conn_type
}; };
} }
@ -192,9 +201,9 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
.set(Flags::HEAD, head.as_ref().method == Method::HEAD); .set(Flags::HEAD, head.as_ref().method == Method::HEAD);
// connection status // connection status
inner.ctype = match head.as_ref().connection_type() { inner.conn_type = match head.as_ref().connection_type() {
ConnectionType::KeepAlive => { ConnectionType::KeepAlive => {
if inner.flags.contains(Flags::KEEPALIVE_ENABLED) { if inner.flags.contains(Flags::KEEP_ALIVE_ENABLED) {
ConnectionType::KeepAlive ConnectionType::KeepAlive
} else { } else {
ConnectionType::Close ConnectionType::Close
@ -211,7 +220,7 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
false, false,
inner.version, inner.version,
length, length,
inner.ctype, inner.conn_type,
&inner.config, &inner.config,
)?; )?;
} }

View File

@ -16,7 +16,7 @@ use crate::{
bitflags! { bitflags! {
struct Flags: u8 { struct Flags: u8 {
const HEAD = 0b0000_0001; const HEAD = 0b0000_0001;
const KEEPALIVE_ENABLED = 0b0000_0010; const KEEP_ALIVE_ENABLED = 0b0000_0010;
const STREAM = 0b0000_0100; const STREAM = 0b0000_0100;
} }
} }
@ -42,7 +42,9 @@ impl Default for Codec {
impl fmt::Debug for Codec { impl fmt::Debug for Codec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 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. /// `keepalive_enabled` how response `connection` header get generated.
pub fn new(config: ServiceConfig) -> Self { pub fn new(config: ServiceConfig) -> Self {
let flags = if config.keep_alive_enabled() { let flags = if config.keep_alive().enabled() {
Flags::KEEPALIVE_ENABLED Flags::KEEP_ALIVE_ENABLED
} else { } else {
Flags::empty() Flags::empty()
}; };
@ -76,14 +78,14 @@ impl Codec {
/// Check if last response is keep-alive. /// Check if last response is keep-alive.
#[inline] #[inline]
pub fn keepalive(&self) -> bool { pub fn keep_alive(&self) -> bool {
self.conn_type == ConnectionType::KeepAlive self.conn_type == ConnectionType::KeepAlive
} }
/// Check if keep-alive enabled on server level. /// Check if keep-alive enabled on server level.
#[inline] #[inline]
pub fn keepalive_enabled(&self) -> bool { pub fn keep_alive_enabled(&self) -> bool {
self.flags.contains(Flags::KEEPALIVE_ENABLED) self.flags.contains(Flags::KEEP_ALIVE_ENABLED)
} }
/// Check last request's message type. /// Check last request's message type.
@ -124,7 +126,7 @@ impl Decoder for Codec {
self.version = head.version; self.version = head.version;
self.conn_type = head.connection_type(); self.conn_type = head.connection_type();
if self.conn_type == ConnectionType::KeepAlive if self.conn_type == ConnectionType::KeepAlive
&& !self.flags.contains(Flags::KEEPALIVE_ENABLED) && !self.flags.contains(Flags::KEEP_ALIVE_ENABLED)
{ {
self.conn_type = ConnectionType::Close self.conn_type = ConnectionType::Close
} }
@ -179,9 +181,11 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
&self.config, &self.config,
)?; )?;
} }
Message::Chunk(Some(bytes)) => { Message::Chunk(Some(bytes)) => {
self.encoder.encode_chunk(bytes.as_ref(), dst)?; self.encoder.encode_chunk(bytes.as_ref(), dst)?;
} }
Message::Chunk(None) => { Message::Chunk(None) => {
self.encoder.encode_eof(dst)?; self.encoder.encode_eof(dst)?;
} }

View File

@ -380,34 +380,36 @@ impl HeaderIndex {
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
/// Http payload item /// Chunk type yielded while decoding a payload.
pub enum PayloadItem { pub enum PayloadItem {
Chunk(Bytes), Chunk(Bytes),
Eof, Eof,
} }
/// Decoders to handle different Transfer-Encodings. /// Decoder that can handle different payload types.
/// ///
/// If a message body does not include a Transfer-Encoding, it *should* /// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
/// include a Content-Length header.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct PayloadDecoder { pub struct PayloadDecoder {
kind: Kind, kind: Kind,
} }
impl PayloadDecoder { impl PayloadDecoder {
/// Constructs a fixed-length payload decoder.
pub fn length(x: u64) -> PayloadDecoder { pub fn length(x: u64) -> PayloadDecoder {
PayloadDecoder { PayloadDecoder {
kind: Kind::Length(x), kind: Kind::Length(x),
} }
} }
/// Constructs a chunked encoding decoder.
pub fn chunked() -> PayloadDecoder { pub fn chunked() -> PayloadDecoder {
PayloadDecoder { PayloadDecoder {
kind: Kind::Chunked(ChunkedState::Size, 0), kind: Kind::Chunked(ChunkedState::Size, 0),
} }
} }
/// Creates an decoder that yields chunks until the stream returns EOF.
pub fn eof() -> PayloadDecoder { pub fn eof() -> PayloadDecoder {
PayloadDecoder { kind: Kind::Eof } PayloadDecoder { kind: Kind::Eof }
} }
@ -415,25 +417,26 @@ impl PayloadDecoder {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
enum Kind { enum Kind {
/// A Reader used when a Content-Length header is passed with a positive /// A reader used when a `Content-Length` header is passed with a positive integer.
/// integer.
Length(u64), Length(u64),
/// A Reader used when Transfer-Encoding is `chunked`.
/// A reader used when `Transfer-Encoding` is `chunked`.
Chunked(ChunkedState, u64), Chunked(ChunkedState, u64),
/// A Reader used for responses that don't indicate a length or chunked.
/// A reader used for responses that don't indicate a length or chunked.
/// ///
/// Note: This should only used for `Response`s. It is illegal for a /// Note: This should only used for `Response`s. It is illegal for a `Request` to be made
/// `Request` to be made with both `Content-Length` and /// without either of `Content-Length` and `Transfer-Encoding: chunked` missing, as explained
/// `Transfer-Encoding: chunked` missing, as explained from the spec: /// in [RFC 7230 §3.3.3]:
/// ///
/// > If a Transfer-Encoding header field is present in a response and /// > If a Transfer-Encoding header field is present in a response and the chunked transfer
/// > the chunked transfer coding is not the final encoding, the /// > coding is not the final encoding, the message body length is determined by reading the
/// > message body length is determined by reading the connection until /// > connection until it is closed by the server. If a Transfer-Encoding header field is
/// > it is closed by the server. If a Transfer-Encoding header field /// > present in a request and the chunked transfer coding is not the final encoding, the
/// > is present in a request and the chunked transfer coding is not /// > message body length cannot be determined reliably; the server MUST respond with the 400
/// > the final encoding, the message body length cannot be determined /// > (Bad Request) status code and then close the connection.
/// > reliably; the server MUST respond with the 400 (Bad Request) ///
/// > status code and then close the connection. /// [RFC 7230 §3.3.3]: https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
Eof, Eof,
} }
@ -463,6 +466,7 @@ impl Decoder for PayloadDecoder {
Ok(Some(PayloadItem::Chunk(buf))) Ok(Some(PayloadItem::Chunk(buf)))
} }
} }
Kind::Chunked(ref mut state, ref mut size) => { Kind::Chunked(ref mut state, ref mut size) => {
loop { loop {
let mut buf = None; let mut buf = None;
@ -488,6 +492,7 @@ impl Decoder for PayloadDecoder {
} }
} }
} }
Kind::Eof => { Kind::Eof => {
if src.is_empty() { if src.is_empty() {
Ok(None) Ok(None)

File diff suppressed because it is too large Load Diff

View 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;
}

View File

@ -105,7 +105,7 @@ pub(crate) trait MessageType: Sized {
} }
BodySize::Sized(0) if camel_case => dst.put_slice(b"\r\nContent-Length: 0\r\n"), BodySize::Sized(0) if camel_case => dst.put_slice(b"\r\nContent-Length: 0\r\n"),
BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"), BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"),
BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::Sized(len) => helpers::write_content_length(len, dst, camel_case),
BodySize::None => dst.put_slice(b"\r\n"), BodySize::None => dst.put_slice(b"\r\n"),
} }
@ -152,7 +152,6 @@ pub(crate) trait MessageType: Sized {
let k = key.as_str().as_bytes(); let k = key.as_str().as_bytes();
let k_len = k.len(); let k_len = k.len();
// TODO: drain?
for val in value.iter() { for val in value.iter() {
let v = val.as_ref(); let v = val.as_ref();
let v_len = v.len(); let v_len = v.len();
@ -213,7 +212,7 @@ pub(crate) trait MessageType: Sized {
// optimized date header, set_date writes \r\n // optimized date header, set_date writes \r\n
if !has_date { if !has_date {
config.set_date(dst); config.write_date_header(dst, camel_case);
} else { } else {
// msg eof // msg eof
dst.extend_from_slice(b"\r\n"); dst.extend_from_slice(b"\r\n");
@ -258,6 +257,12 @@ impl MessageType for Response<()> {
None None
} }
fn camel_case(&self) -> bool {
self.head()
.flags
.contains(crate::message::Flags::CAMEL_CASE)
}
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> { fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> {
let head = self.head(); let head = self.head();
let reason = head.reason().as_bytes(); let reason = head.reason().as_bytes();
@ -313,16 +318,17 @@ impl MessageType for RequestHeadType {
} }
impl<T: MessageType> MessageEncoder<T> { impl<T: MessageType> MessageEncoder<T> {
/// Encode message /// Encode chunk.
pub fn encode_chunk(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result<bool> { pub fn encode_chunk(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result<bool> {
self.te.encode(msg, buf) self.te.encode(msg, buf)
} }
/// Encode eof /// Encode EOF.
pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> { pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> {
self.te.encode_eof(buf) self.te.encode_eof(buf)
} }
/// Encode message.
pub fn encode( pub fn encode(
&mut self, &mut self,
dst: &mut BytesMut, dst: &mut BytesMut,

View File

@ -7,10 +7,13 @@ mod client;
mod codec; mod codec;
mod decoder; mod decoder;
mod dispatcher; mod dispatcher;
#[cfg(test)]
mod dispatcher_tests;
mod encoder; mod encoder;
mod expect; mod expect;
mod payload; mod payload;
mod service; mod service;
mod timer;
mod upgrade; mod upgrade;
mod utils; mod utils;
@ -26,9 +29,10 @@ pub use self::utils::SendResponse;
#[derive(Debug)] #[derive(Debug)]
/// Codec message /// Codec message
pub enum Message<T> { pub enum Message<T> {
/// Http message /// HTTP message.
Item(T), Item(T),
/// Payload chunk
/// Payload chunk.
Chunk(Option<Bytes>), Chunk(Option<Bytes>),
} }

View 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)
)
}
}
}
}
}

View File

@ -57,11 +57,11 @@ where
conn_data: OnConnectData, conn_data: OnConnectData,
timer: Option<Pin<Box<Sleep>>>, timer: Option<Pin<Box<Sleep>>>,
) -> Self { ) -> Self {
let ping_pong = config.keep_alive().map(|dur| H2PingPong { let ping_pong = config.keep_alive().duration().map(|dur| H2PingPong {
timer: timer timer: timer
.map(|mut timer| { .map(|mut timer| {
// reset timer if it's received from new function. // reuse timer slot if it was initialized for handshake
timer.as_mut().reset(config.now() + dur); timer.as_mut().reset((config.now() + dur).into());
timer timer
}) })
.unwrap_or_else(|| Box::pin(sleep(dur))), .unwrap_or_else(|| Box::pin(sleep(dur))),
@ -141,7 +141,7 @@ where
DispatchError::SendResponse(err) => { DispatchError::SendResponse(err) => {
trace!("Error sending HTTP/2 response: {:?}", err) trace!("Error sending HTTP/2 response: {:?}", err)
} }
DispatchError::SendData(err) => warn!("{:?}", err), DispatchError::SendData(err) => log::warn!("{:?}", err),
DispatchError::ResponseBody(err) => { DispatchError::ResponseBody(err) => {
error!("Response payload stream error: {:?}", err) error!("Response payload stream error: {:?}", err)
} }
@ -160,8 +160,8 @@ where
Poll::Ready(_) => { Poll::Ready(_) => {
ping_pong.on_flight = false; ping_pong.on_flight = false;
let dead_line = this.config.keep_alive_expire().unwrap(); let dead_line = this.config.keep_alive_deadline().unwrap();
ping_pong.timer.as_mut().reset(dead_line); ping_pong.timer.as_mut().reset(dead_line.into());
} }
Poll::Pending => { Poll::Pending => {
return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(())) return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(()))
@ -174,8 +174,8 @@ where
ping_pong.ping_pong.send_ping(Ping::opaque())?; ping_pong.ping_pong.send_ping(Ping::opaque())?;
let dead_line = this.config.keep_alive_expire().unwrap(); let dead_line = this.config.keep_alive_deadline().unwrap();
ping_pong.timer.as_mut().reset(dead_line); ping_pong.timer.as_mut().reset(dead_line.into());
ping_pong.on_flight = true; ping_pong.on_flight = true;
} }
@ -322,7 +322,7 @@ fn prepare_response(
// set date header // set date header
if !has_date { if !has_date {
let mut bytes = BytesMut::with_capacity(29); let mut bytes = BytesMut::with_capacity(29);
config.set_date_header(&mut bytes); config.write_date_header_value(&mut bytes);
res.headers_mut().insert( res.headers_mut().insert(
DATE, DATE,
// SAFETY: serialized date-times are known ASCII strings // SAFETY: serialized date-times are known ASCII strings

View File

@ -7,7 +7,7 @@ use std::{
}; };
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::Sleep; use actix_rt::time::{sleep_until, Sleep};
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use h2::{ use h2::{
@ -15,17 +15,17 @@ use h2::{
RecvStream, RecvStream,
}; };
use crate::{
config::ServiceConfig,
error::{DispatchError, PayloadError},
};
mod dispatcher; mod dispatcher;
mod service; mod service;
pub use self::dispatcher::Dispatcher; pub use self::dispatcher::Dispatcher;
pub use self::service::H2Service; pub use self::service::H2Service;
use crate::{
config::ServiceConfig,
error::{DispatchError, PayloadError},
};
/// HTTP/2 peer stream. /// HTTP/2 peer stream.
pub struct Payload { pub struct Payload {
stream: RecvStream, stream: RecvStream,
@ -67,7 +67,9 @@ where
{ {
HandshakeWithTimeout { HandshakeWithTimeout {
handshake: handshake(io), 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(); let this = self.get_mut();
match Pin::new(&mut this.handshake).poll(cx)? { 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::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))),
Poll::Pending => match this.timer.as_mut() { Poll::Pending => match this.timer.as_mut() {
Some(timer) => { Some(timer) => {

View File

@ -355,7 +355,7 @@ where
} }
Err(err) => { Err(err) => {
trace!("H2 handshake error: {}", err); log::trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err)) Poll::Ready(Err(err))
} }
}, },

View File

@ -630,7 +630,7 @@ impl Removed {
/// Returns true if iterator contains no elements, without consuming it. /// Returns true if iterator contains no elements, without consuming it.
/// ///
/// If called immediately after [`HeaderMap::insert`] or [`HeaderMap::remove`], it will indicate /// 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 { pub fn is_empty(&self) -> bool {
match self.inner { match self.inner {
// size hint lower bound of smallvec is the correct length // size hint lower bound of smallvec is the correct length

View File

@ -4,8 +4,7 @@ use bytes::BytesMut;
use http::header::{HeaderValue, InvalidHeaderValue}; use http::header::{HeaderValue, InvalidHeaderValue};
use crate::{ use crate::{
config::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue, date::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue, helpers::MutWriter,
helpers::MutWriter,
}; };
/// A timestamp with HTTP-style formatting and parsing. /// A timestamp with HTTP-style formatting and parsing.

View File

@ -30,15 +30,25 @@ pub(crate) fn write_status_line<B: BufMut>(version: Version, n: u16, buf: &mut B
/// Write out content length header. /// Write out content length header.
/// ///
/// Buffer must to contain enough space or be implicitly extendable. /// Buffer must to contain enough space or be implicitly extendable.
pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B) { pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B, camel_case: bool) {
if n == 0 { if n == 0 {
if camel_case {
buf.put_slice(b"\r\nContent-Length: 0\r\n");
} else {
buf.put_slice(b"\r\ncontent-length: 0\r\n"); buf.put_slice(b"\r\ncontent-length: 0\r\n");
}
return; return;
} }
let mut buffer = itoa::Buffer::new(); let mut buffer = itoa::Buffer::new();
if camel_case {
buf.put_slice(b"\r\nContent-Length: ");
} else {
buf.put_slice(b"\r\ncontent-length: "); buf.put_slice(b"\r\ncontent-length: ");
}
buf.put_slice(buffer.format(n).as_bytes()); buf.put_slice(buffer.format(n).as_bytes());
buf.put_slice(b"\r\n"); buf.put_slice(b"\r\n");
} }
@ -95,77 +105,88 @@ mod tests {
fn test_write_content_length() { fn test_write_content_length() {
let mut bytes = BytesMut::new(); let mut bytes = BytesMut::new();
bytes.reserve(50); bytes.reserve(50);
write_content_length(0, &mut bytes); write_content_length(0, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 0\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 0\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(9, &mut bytes); write_content_length(9, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(10, &mut bytes); write_content_length(10, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(99, &mut bytes); write_content_length(99, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(100, &mut bytes); write_content_length(100, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 100\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 100\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(101, &mut bytes); write_content_length(101, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 101\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 101\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(998, &mut bytes); write_content_length(998, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 998\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 998\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(1000, &mut bytes); write_content_length(1000, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1000\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1000\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(1001, &mut bytes); write_content_length(1001, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1001\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1001\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(5909, &mut bytes); write_content_length(5909, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 5909\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 5909\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(9999, &mut bytes); write_content_length(9999, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9999\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9999\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(10001, &mut bytes); write_content_length(10001, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10001\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10001\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(59094, &mut bytes); write_content_length(59094, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 59094\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 59094\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(99999, &mut bytes); write_content_length(99999, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99999\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99999\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(590947, &mut bytes); write_content_length(590947, &mut bytes, false);
assert_eq!( assert_eq!(
bytes.split().freeze(), bytes.split().freeze(),
b"\r\ncontent-length: 590947\r\n"[..] b"\r\ncontent-length: 590947\r\n"[..]
); );
bytes.reserve(50); bytes.reserve(50);
write_content_length(999999, &mut bytes); write_content_length(999999, &mut bytes, false);
assert_eq!( assert_eq!(
bytes.split().freeze(), bytes.split().freeze(),
b"\r\ncontent-length: 999999\r\n"[..] b"\r\ncontent-length: 999999\r\n"[..]
); );
bytes.reserve(50); bytes.reserve(50);
write_content_length(5909471, &mut bytes); write_content_length(5909471, &mut bytes, false);
assert_eq!( assert_eq!(
bytes.split().freeze(), bytes.split().freeze(),
b"\r\ncontent-length: 5909471\r\n"[..] b"\r\ncontent-length: 5909471\r\n"[..]
); );
bytes.reserve(50); bytes.reserve(50);
write_content_length(59094718, &mut bytes); write_content_length(59094718, &mut bytes, false);
assert_eq!( assert_eq!(
bytes.split().freeze(), bytes.split().freeze(),
b"\r\ncontent-length: 59094718\r\n"[..] b"\r\ncontent-length: 59094718\r\n"[..]
); );
bytes.reserve(50); bytes.reserve(50);
write_content_length(4294973728, &mut bytes); write_content_length(4294973728, &mut bytes, false);
assert_eq!( assert_eq!(
bytes.split().freeze(), bytes.split().freeze(),
b"\r\ncontent-length: 4294973728\r\n"[..] b"\r\ncontent-length: 4294973728\r\n"[..]
); );
} }
#[test]
fn write_content_length_camel_case() {
let mut bytes = BytesMut::new();
write_content_length(0, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 0\r\n"[..]);
let mut bytes = BytesMut::new();
write_content_length(0, &mut bytes, true);
assert_eq!(bytes.split().freeze(), b"\r\nContent-Length: 0\r\n"[..]);
}
} }

View File

@ -25,10 +25,10 @@ pub trait HttpMessage: Sized {
/// Message payload stream /// Message payload stream
fn take_payload(&mut self) -> Payload<Self::Stream>; fn take_payload(&mut self) -> Payload<Self::Stream>;
/// Request's extensions container /// Returns a reference to the request-local data/extensions container.
fn extensions(&self) -> Ref<'_, Extensions>; fn extensions(&self) -> Ref<'_, Extensions>;
/// Mutable reference to a the request's extensions container /// Returns a mutable reference to the request-local data/extensions container.
fn extensions_mut(&self) -> RefMut<'_, Extensions>; fn extensions_mut(&self) -> RefMut<'_, Extensions>;
/// Get a header. /// Get a header.
@ -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. /// UTF-8 is used by default, If request charset is not set.
fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> { fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> {

View 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);
}
}

View File

@ -24,38 +24,42 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#[macro_use]
extern crate log;
pub use ::http::{uri, uri::Uri}; pub use ::http::{uri, uri::Uri};
pub use ::http::{Method, StatusCode, Version}; pub use ::http::{Method, StatusCode, Version};
pub mod body; pub mod body;
mod builder; mod builder;
mod config; mod config;
mod date;
#[cfg(feature = "__compress")] #[cfg(feature = "__compress")]
pub mod encoding; pub mod encoding;
pub mod error; pub mod error;
mod extensions; mod extensions;
pub mod h1; pub mod h1;
#[cfg(feature = "http2")]
pub mod h2; pub mod h2;
pub mod header; pub mod header;
mod helpers; mod helpers;
mod http_message; mod http_message;
mod keep_alive;
mod message; mod message;
#[cfg(test)]
mod notify_on_drop;
mod payload; mod payload;
mod requests; mod requests;
mod responses; mod responses;
mod service; mod service;
pub mod test; pub mod test;
#[cfg(feature = "ws")]
pub mod ws; pub mod ws;
pub use self::builder::HttpServiceBuilder; pub use self::builder::HttpServiceBuilder;
pub use self::config::{KeepAlive, ServiceConfig}; pub use self::config::ServiceConfig;
pub use self::error::Error; pub use self::error::Error;
pub use self::extensions::Extensions; pub use self::extensions::Extensions;
pub use self::header::ContentEncoding; pub use self::header::ContentEncoding;
pub use self::http_message::HttpMessage; pub use self::http_message::HttpMessage;
pub use self::keep_alive::KeepAlive;
pub use self::message::ConnectionType; pub use self::message::ConnectionType;
pub use self::message::Message; pub use self::message::Message;
#[allow(deprecated)] #[allow(deprecated)]

View File

@ -5,13 +5,13 @@ use bitflags::bitflags;
/// Represents various types of connection /// Represents various types of connection
#[derive(Copy, Clone, PartialEq, Debug)] #[derive(Copy, Clone, PartialEq, Debug)]
pub enum ConnectionType { pub enum ConnectionType {
/// Close connection after response /// Close connection after response.
Close, Close,
/// Keep connection alive after response /// Keep connection alive after response.
KeepAlive, KeepAlive,
/// Connection is upgraded to different type /// Connection is upgraded to different type.
Upgrade, Upgrade,
} }
@ -69,8 +69,8 @@ impl<T: Head> Drop for Message<T> {
} }
} }
/// Generic `Head` object pool.
#[doc(hidden)] #[doc(hidden)]
/// Request's objects pool
pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>); pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
impl<T: Head> MessagePool<T> { impl<T: Head> MessagePool<T> {

View 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;
}
});
}
}

View File

@ -6,6 +6,7 @@ use std::{
use bytes::Bytes; use bytes::Bytes;
use futures_core::Stream; use futures_core::Stream;
use pin_project_lite::pin_project;
use crate::error::PayloadError; 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`.")] #[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")]
pub type PayloadStream = 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. /// A streaming payload.
#[project = PayloadProj] #[project = PayloadProj]
pub enum Payload<S = BoxedPayloadStream> { 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> { impl<S> From<crate::h2::Payload> for Payload<S> {
fn from(payload: crate::h2::Payload) -> Self { fn from(payload: crate::h2::Payload) -> Self {
Payload::H2 { payload } Payload::H2 { payload }
} }
} }
impl<S> From<h2::RecvStream> for Payload<S> { #[cfg(feature = "http2")]
fn from(stream: h2::RecvStream) -> Self { impl<S> From<::h2::RecvStream> for Payload<S> {
fn from(stream: ::h2::RecvStream) -> Self {
Payload::H2 { Payload::H2 {
payload: crate::h2::Payload::new(stream), payload: crate::h2::Payload::new(stream),
} }
@ -70,7 +85,10 @@ where
match self.project() { match self.project() {
PayloadProj::None => Poll::Ready(None), PayloadProj::None => Poll::Ready(None),
PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx), PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx),
#[cfg(feature = "http2")]
PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx), PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx),
PayloadProj::Stream { payload } => payload.poll_next(cx), PayloadProj::Stream { payload } => payload.poll_next(cx),
} }
} }

View File

@ -130,8 +130,8 @@ impl RequestHead {
} }
} }
/// Request contains `EXPECT` header.
#[inline] #[inline]
/// Request contains `EXPECT` header
pub fn expect(&self) -> bool { pub fn expect(&self) -> bool {
self.flags.contains(Flags::EXPECT) self.flags.contains(Flags::EXPECT)
} }
@ -142,8 +142,8 @@ impl RequestHead {
} }
} }
#[derive(Debug)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum RequestHeadType { pub enum RequestHeadType {
Owned(RequestHead), Owned(RequestHead),
Rc(Rc<RequestHead>, Option<HeaderMap>), Rc(Rc<RequestHead>, Option<HeaderMap>),

View File

@ -19,7 +19,7 @@ pub struct Request<P = BoxedPayloadStream> {
pub(crate) payload: Payload<P>, pub(crate) payload: Payload<P>,
pub(crate) head: Message<RequestHead>, pub(crate) head: Message<RequestHead>,
pub(crate) conn_data: Option<Rc<Extensions>>, pub(crate) conn_data: Option<Rc<Extensions>>,
pub(crate) req_data: RefCell<Extensions>, pub(crate) extensions: RefCell<Extensions>,
} }
impl<P> HttpMessage for Request<P> { impl<P> HttpMessage for Request<P> {
@ -34,16 +34,14 @@ impl<P> HttpMessage for Request<P> {
mem::replace(&mut self.payload, Payload::None) mem::replace(&mut self.payload, Payload::None)
} }
/// Request extensions
#[inline] #[inline]
fn extensions(&self) -> Ref<'_, Extensions> { fn extensions(&self) -> Ref<'_, Extensions> {
self.req_data.borrow() self.extensions.borrow()
} }
/// Mutable reference to a the request's extensions
#[inline] #[inline]
fn extensions_mut(&self) -> RefMut<'_, Extensions> { fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.req_data.borrow_mut() self.extensions.borrow_mut()
} }
} }
@ -52,7 +50,7 @@ impl From<Message<RequestHead>> for Request<BoxedPayloadStream> {
Request { Request {
head, head,
payload: Payload::None, payload: Payload::None,
req_data: RefCell::new(Extensions::default()), extensions: RefCell::new(Extensions::default()),
conn_data: None, conn_data: None,
} }
} }
@ -65,7 +63,7 @@ impl Request<BoxedPayloadStream> {
Request { Request {
head: Message::new(), head: Message::new(),
payload: Payload::None, payload: Payload::None,
req_data: RefCell::new(Extensions::default()), extensions: RefCell::new(Extensions::default()),
conn_data: None, conn_data: None,
} }
} }
@ -77,7 +75,7 @@ impl<P> Request<P> {
Request { Request {
payload, payload,
head: Message::new(), head: Message::new(),
req_data: RefCell::new(Extensions::default()), extensions: RefCell::new(Extensions::default()),
conn_data: None, conn_data: None,
} }
} }
@ -90,7 +88,7 @@ impl<P> Request<P> {
Request { Request {
payload, payload,
head: self.head, head: self.head,
req_data: self.req_data, extensions: self.extensions,
conn_data: self.conn_data, conn_data: self.conn_data,
}, },
pl, pl,
@ -195,16 +193,17 @@ impl<P> Request<P> {
.and_then(|container| container.get::<T>()) .and_then(|container| container.get::<T>())
} }
/// Returns the connection data container if an [on-connect] callback was registered. /// Returns the connection-level data/extensions container if an [on-connect] callback was
/// registered, leaving an empty one in its place.
/// ///
/// [on-connect]: crate::HttpServiceBuilder::on_connect_ext /// [on-connect]: crate::HttpServiceBuilder::on_connect_ext
pub fn take_conn_data(&mut self) -> Option<Rc<Extensions>> { pub fn take_conn_data(&mut self) -> Option<Rc<Extensions>> {
self.conn_data.take() self.conn_data.take()
} }
/// Returns the request data container, leaving an empty one in it's place. /// Returns the request-local data/extensions container, leaving an empty one in its place.
pub fn take_req_data(&mut self) -> Extensions { pub fn take_req_data(&mut self) -> Extensions {
mem::take(self.req_data.get_mut()) mem::take(self.extensions.get_mut())
} }
} }

View File

@ -1,9 +1,6 @@
//! HTTP response builder. //! HTTP response builder.
use std::{ use std::{cell::RefCell, fmt, str};
cell::{Ref, RefMut},
fmt, str,
};
use crate::{ use crate::{
body::{EitherBody, MessageBody}, body::{EitherBody, MessageBody},
@ -202,20 +199,6 @@ impl ResponseBuilder {
self self
} }
/// Responses extensions
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
let head = self.head.as_ref().expect("cannot reuse response builder");
head.extensions.borrow()
}
/// Mutable reference to a the response's extensions
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
let head = self.head.as_ref().expect("cannot reuse response builder");
head.extensions.borrow_mut()
}
/// Generate response with a wrapped body. /// Generate response with a wrapped body.
/// ///
/// This `ResponseBuilder` will be left in a useless state. /// This `ResponseBuilder` will be left in a useless state.
@ -238,7 +221,12 @@ impl ResponseBuilder {
} }
let head = self.head.take().expect("cannot reuse response builder"); let head = self.head.take().expect("cannot reuse response builder");
Ok(Response { head, body })
Ok(Response {
head,
body,
extensions: RefCell::new(Extensions::new()),
})
} }
/// Generate response with an empty body. /// Generate response with an empty body.

View File

@ -1,26 +1,20 @@
//! Response head type and caching pool. //! Response head type and caching pool.
use std::{ use std::{cell::RefCell, ops};
cell::{Ref, RefCell, RefMut},
ops,
};
use crate::{ use crate::{header::HeaderMap, message::Flags, ConnectionType, StatusCode, Version};
header::HeaderMap, message::Flags, ConnectionType, Extensions, StatusCode, Version,
};
thread_local! { thread_local! {
static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create(); static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create();
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct ResponseHead { pub struct ResponseHead {
pub version: Version, pub version: Version,
pub status: StatusCode, pub status: StatusCode,
pub headers: HeaderMap, pub headers: HeaderMap,
pub reason: Option<&'static str>, pub reason: Option<&'static str>,
pub(crate) extensions: RefCell<Extensions>, pub(crate) flags: Flags,
flags: Flags,
} }
impl ResponseHead { impl ResponseHead {
@ -33,36 +27,35 @@ impl ResponseHead {
headers: HeaderMap::with_capacity(12), headers: HeaderMap::with_capacity(12),
reason: None, reason: None,
flags: Flags::empty(), flags: Flags::empty(),
extensions: RefCell::new(Extensions::new()),
} }
} }
#[inline]
/// Read the message headers. /// Read the message headers.
#[inline]
pub fn headers(&self) -> &HeaderMap { pub fn headers(&self) -> &HeaderMap {
&self.headers &self.headers
} }
#[inline]
/// Mutable reference to the message headers. /// Mutable reference to the message headers.
#[inline]
pub fn headers_mut(&mut self) -> &mut HeaderMap { pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers &mut self.headers
} }
/// Message extensions /// 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] #[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> { pub fn set_camel_case_headers(&mut self, camel_case: bool) {
self.extensions.borrow() if camel_case {
self.flags.insert(Flags::CAMEL_CASE);
} else {
self.flags.remove(Flags::CAMEL_CASE);
}
} }
/// Mutable reference to a the message's extensions
#[inline]
pub fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.extensions.borrow_mut()
}
#[inline]
/// Set connection type of the message /// Set connection type of the message
#[inline]
pub fn set_connection_type(&mut self, ctype: ConnectionType) { pub fn set_connection_type(&mut self, ctype: ConnectionType) {
match ctype { match ctype {
ConnectionType::Close => self.flags.insert(Flags::CLOSE), ConnectionType::Close => self.flags.insert(Flags::CLOSE),
@ -121,14 +114,14 @@ impl ResponseHead {
} }
} }
#[inline]
/// Get response body chunking state /// Get response body chunking state
#[inline]
pub fn chunked(&self) -> bool { pub fn chunked(&self) -> bool {
!self.flags.contains(Flags::NO_CHUNKING) !self.flags.contains(Flags::NO_CHUNKING)
} }
#[inline]
/// Set no chunking for payload /// Set no chunking for payload
#[inline]
pub fn no_chunking(&mut self, val: bool) { pub fn no_chunking(&mut self, val: bool) {
if val { if val {
self.flags.insert(Flags::NO_CHUNKING); self.flags.insert(Flags::NO_CHUNKING);
@ -171,7 +164,7 @@ impl Drop for BoxedResponseHead {
} }
} }
/// Request's objects pool /// Response head object pool.
#[doc(hidden)] #[doc(hidden)]
pub struct BoxedResponsePool(#[allow(clippy::vec_box)] RefCell<Vec<Box<ResponseHead>>>); pub struct BoxedResponsePool(#[allow(clippy::vec_box)] RefCell<Vec<Box<ResponseHead>>>);
@ -180,7 +173,7 @@ impl BoxedResponsePool {
BoxedResponsePool(RefCell::new(Vec::with_capacity(128))) BoxedResponsePool(RefCell::new(Vec::with_capacity(128)))
} }
/// Get message from the pool /// Get message from the pool.
#[inline] #[inline]
fn get_message(&self, status: StatusCode) -> BoxedResponseHead { fn get_message(&self, status: StatusCode) -> BoxedResponseHead {
if let Some(mut head) = self.0.borrow_mut().pop() { if let Some(mut head) = self.0.borrow_mut().pop() {
@ -196,13 +189,81 @@ impl BoxedResponsePool {
} }
} }
/// Release request instance /// Release request instance.
#[inline] #[inline]
fn release(&self, mut msg: Box<ResponseHead>) { fn release(&self, msg: Box<ResponseHead>) {
let pool = &mut self.0.borrow_mut(); let pool = &mut self.0.borrow_mut();
if pool.len() < 128 { if pool.len() < 128 {
msg.extensions.get_mut().clear();
pool.push(msg); pool.push(msg);
} }
} }
} }
#[cfg(test)]
mod tests {
use std::{
io::{Read as _, Write as _},
net,
};
use memchr::memmem;
use crate::{
h1::H1Service,
header::{HeaderName, HeaderValue},
Error, Request, Response, ServiceConfig,
};
#[actix_rt::test]
async fn camel_case_headers() {
let mut srv = actix_http_test::test_server(|| {
H1Service::with_config(ServiceConfig::default(), |req: Request| async move {
let mut res = Response::ok();
if req.path().contains("camel") {
res.head_mut().set_camel_case_headers(true);
}
res.headers_mut().insert(
HeaderName::from_static("foo-bar"),
HeaderValue::from_static("baz"),
);
Ok::<_, Error>(res)
})
.tcp()
})
.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")
.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());
assert!(memmem::find(&data, b"Date").is_some());
assert!(memmem::find(&data, b"date").is_none());
assert!(memmem::find(&data, b"Content-Length").is_some());
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")
.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());
assert!(memmem::find(&data, b"Date").is_none());
assert!(memmem::find(&data, b"date").is_some());
assert!(memmem::find(&data, b"Content-Length").is_none());
assert!(memmem::find(&data, b"content-length").is_some());
srv.stop().await;
}
}

View File

@ -1,7 +1,7 @@
//! HTTP response. //! HTTP response.
use std::{ use std::{
cell::{Ref, RefMut}, cell::{Ref, RefCell, RefMut},
fmt, str, fmt, str,
}; };
@ -9,7 +9,7 @@ use bytes::{Bytes, BytesMut};
use bytestring::ByteString; use bytestring::ByteString;
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, EitherBody, MessageBody},
header::{self, HeaderMap, TryIntoHeaderValue}, header::{self, HeaderMap, TryIntoHeaderValue},
responses::BoxedResponseHead, responses::BoxedResponseHead,
Error, Extensions, ResponseBuilder, ResponseHead, StatusCode, Error, Extensions, ResponseBuilder, ResponseHead, StatusCode,
@ -19,6 +19,7 @@ use crate::{
pub struct Response<B> { pub struct Response<B> {
pub(crate) head: BoxedResponseHead, pub(crate) head: BoxedResponseHead,
pub(crate) body: B, pub(crate) body: B,
pub(crate) extensions: RefCell<Extensions>,
} }
impl Response<BoxBody> { impl Response<BoxBody> {
@ -28,6 +29,7 @@ impl Response<BoxBody> {
Response { Response {
head: BoxedResponseHead::new(status), head: BoxedResponseHead::new(status),
body: BoxBody::new(()), body: BoxBody::new(()),
extensions: RefCell::new(Extensions::new()),
} }
} }
@ -74,6 +76,7 @@ impl<B> Response<B> {
Response { Response {
head: BoxedResponseHead::new(status), head: BoxedResponseHead::new(status),
body, body,
extensions: RefCell::new(Extensions::new()),
} }
} }
@ -120,20 +123,21 @@ impl<B> Response<B> {
} }
/// Returns true if keep-alive is enabled. /// Returns true if keep-alive is enabled.
#[inline]
pub fn keep_alive(&self) -> bool { pub fn keep_alive(&self) -> bool {
self.head.keep_alive() self.head.keep_alive()
} }
/// Returns a reference to the extensions of this response. /// Returns a reference to the request-local data/extensions container.
#[inline] #[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> { pub fn extensions(&self) -> Ref<'_, Extensions> {
self.head.extensions.borrow() self.extensions.borrow()
} }
/// Returns a mutable reference to the extensions of this response. /// Returns a mutable reference to the request-local data/extensions container.
#[inline] #[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
self.head.extensions.borrow_mut() self.extensions.borrow_mut()
} }
/// Returns a reference to the body of this response. /// Returns a reference to the body of this response.
@ -143,24 +147,29 @@ impl<B> Response<B> {
} }
/// Sets new body. /// Sets new body.
#[inline]
pub fn set_body<B2>(self, body: B2) -> Response<B2> { pub fn set_body<B2>(self, body: B2) -> Response<B2> {
Response { Response {
head: self.head, head: self.head,
body, body,
extensions: self.extensions,
} }
} }
/// Drops body and returns new response. /// Drops body and returns new response.
#[inline]
pub fn drop_body(self) -> Response<()> { pub fn drop_body(self) -> Response<()> {
self.set_body(()) self.set_body(())
} }
/// Sets new body, returning new response and previous body value. /// Sets new body, returning new response and previous body value.
#[inline]
pub(crate) fn replace_body<B2>(self, body: B2) -> (Response<B2>, B) { pub(crate) fn replace_body<B2>(self, body: B2) -> (Response<B2>, B) {
( (
Response { Response {
head: self.head, head: self.head,
body, body,
extensions: self.extensions,
}, },
self.body, self.body,
) )
@ -171,11 +180,15 @@ impl<B> Response<B> {
/// # Implementation Notes /// # Implementation Notes
/// Due to internal performance optimizations, the first element of the returned tuple is a /// Due to internal performance optimizations, the first element of the returned tuple is a
/// `Response` as well but only contains the head of the response this was called on. /// `Response` as well but only contains the head of the response this was called on.
#[inline]
pub fn into_parts(self) -> (Response<()>, B) { pub fn into_parts(self) -> (Response<()>, B) {
self.replace_body(()) self.replace_body(())
} }
/// Returns new response with mapped body. /// 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]
pub fn map_body<F, B2>(mut self, f: F) -> Response<B2> pub fn map_body<F, B2>(mut self, f: F) -> Response<B2>
where where
F: FnOnce(&mut ResponseHead, B) -> B2, F: FnOnce(&mut ResponseHead, B) -> B2,
@ -185,9 +198,11 @@ impl<B> Response<B> {
Response { Response {
head: self.head, head: self.head,
body, body,
extensions: self.extensions,
} }
} }
/// Map the current body to a type-erased `BoxBody`.
#[inline] #[inline]
pub fn map_into_boxed_body(self) -> Response<BoxBody> pub fn map_into_boxed_body(self) -> Response<BoxBody>
where where
@ -196,7 +211,8 @@ impl<B> Response<B> {
self.map_body(|_, body| body.boxed()) 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 { pub fn into_body(self) -> B {
self.body self.body
} }
@ -239,9 +255,9 @@ impl<I: Into<Response<BoxBody>>, E: Into<Error>> From<Result<I, E>> for Response
} }
} }
impl From<ResponseBuilder> for Response<BoxBody> { impl From<ResponseBuilder> for Response<EitherBody<()>> {
fn from(mut builder: ResponseBuilder) -> Self { fn from(mut builder: ResponseBuilder) -> Self {
builder.finish().map_into_boxed_body() builder.finish()
} }
} }

View File

@ -19,9 +19,8 @@ use pin_project_lite::pin_project;
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
builder::HttpServiceBuilder, builder::HttpServiceBuilder,
config::{KeepAlive, ServiceConfig},
error::DispatchError, 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. /// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
@ -43,9 +42,9 @@ where
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
/// Create builder for `HttpService` instance. /// Constructs builder for `HttpService` instance.
pub fn build() -> HttpServiceBuilder<T, S> { pub fn build() -> HttpServiceBuilder<T, S> {
HttpServiceBuilder::new() HttpServiceBuilder::default()
} }
} }
@ -58,12 +57,10 @@ where
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + '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 { pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self {
let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0, false, None);
HttpService { HttpService {
cfg, cfg: ServiceConfig::default(),
srv: service.into_factory(), srv: service.into_factory(),
expect: h1::ExpectHandler, expect: h1::ExpectHandler,
upgrade: None, 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>>( pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
cfg: ServiceConfig, cfg: ServiceConfig,
service: F, service: F,
@ -97,11 +94,10 @@ where
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody, 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. /// An expect service is called with requests that contain an `Expect` header. A successful
/// Service must return request in case of success, in that case /// response type is also a request which will be forwarded to the main service.
/// request will be forwarded to main service.
pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U> pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U>
where where
X1: ServiceFactory<Request, Config = (), Response = Request>, 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 /// If service is provided then normal requests handling get halted and this service get called
/// and this service get called with original request and framed object. /// with original request and framed object.
pub fn upgrade<U1>(self, upgrade: Option<U1>) -> HttpService<T, S, B, X, U1> pub fn upgrade<U1>(self, upgrade: Option<U1>) -> HttpService<T, S, B, X, U1>
where where
U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>, 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()); let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
match proto { match proto {
#[cfg(feature = "http2")]
Protocol::Http2 => HttpServiceHandlerResponse { Protocol::Http2 => HttpServiceHandlerResponse {
state: State::H2Handshake { state: State::H2Handshake {
handshake: Some(( handshake: Some((
h2::handshake_with_timeout(io, &self.cfg), crate::h2::handshake_with_timeout(io, &self.cfg),
self.cfg.clone(), self.cfg.clone(),
self.flow.clone(), self.flow.clone(),
conn_data, 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 { Protocol::Http1 => HttpServiceHandlerResponse {
state: State::H1 { state: State::H1 {
dispatcher: h1::Dispatcher::new( dispatcher: h1::Dispatcher::new(
@ -535,6 +537,7 @@ where
} }
} }
#[cfg(not(feature = "http2"))]
pin_project! { pin_project! {
#[project = StateProj] #[project = StateProj]
enum State<T, S, B, X, U> enum State<T, S, B, X, U>
@ -556,10 +559,37 @@ pin_project! {
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
H1 { #[pin] dispatcher: h1::Dispatcher<T, S, B, X, U> }, 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 { H2Handshake {
handshake: Option<( handshake: Option<(
h2::HandshakeWithTimeout<T>, crate::h2::HandshakeWithTimeout<T>,
ServiceConfig, ServiceConfig,
Rc<HttpFlow<S, X, U>>, Rc<HttpFlow<S, X, U>>,
OnConnectData, OnConnectData,
@ -618,21 +648,25 @@ where
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project().state.project() { match self.as_mut().project().state.project() {
StateProj::H1 { dispatcher } => dispatcher.poll(cx), StateProj::H1 { dispatcher } => dispatcher.poll(cx),
#[cfg(feature = "http2")]
StateProj::H2 { dispatcher } => dispatcher.poll(cx), StateProj::H2 { dispatcher } => dispatcher.poll(cx),
#[cfg(feature = "http2")]
StateProj::H2Handshake { handshake: data } => { StateProj::H2Handshake { handshake: data } => {
match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) { match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) {
Ok((conn, timer)) => { Ok((conn, timer)) => {
let (_, config, flow, conn_data, peer_addr) = data.take().unwrap(); let (_, config, flow, conn_data, peer_addr) = data.take().unwrap();
self.as_mut().project().state.set(State::H2 { 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, conn, flow, config, peer_addr, conn_data, timer,
), ),
}); });
self.poll(cx) self.poll(cx)
} }
Err(err) => { Err(err) => {
trace!("H2 handshake error: {}", err); log::trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err)) Poll::Ready(Err(err))
} }
} }

View File

@ -1,7 +1,7 @@
//! Various testing helpers for use in internal and app tests. //! Various testing helpers for use in internal and app tests.
use std::{ use std::{
cell::{Ref, RefCell}, cell::{Ref, RefCell, RefMut},
io::{self, Read, Write}, io::{self, Read, Write},
pin::Pin, pin::Pin,
rc::Rc, rc::Rc,
@ -157,10 +157,11 @@ fn parts(parts: &mut Option<Inner>) -> &mut Inner {
} }
/// Async I/O test buffer. /// Async I/O test buffer.
#[derive(Debug)]
pub struct TestBuffer { pub struct TestBuffer {
pub read_buf: BytesMut, pub read_buf: Rc<RefCell<BytesMut>>,
pub write_buf: BytesMut, pub write_buf: Rc<RefCell<BytesMut>>,
pub err: Option<io::Error>, pub err: Option<Rc<io::Error>>,
} }
impl TestBuffer { impl TestBuffer {
@ -170,34 +171,69 @@ impl TestBuffer {
T: Into<BytesMut>, T: Into<BytesMut>,
{ {
Self { Self {
read_buf: data.into(), read_buf: Rc::new(RefCell::new(data.into())),
write_buf: BytesMut::new(), write_buf: Rc::new(RefCell::new(BytesMut::new())),
err: None, 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. /// Create new empty `TestBuffer` instance.
pub fn empty() -> Self { pub fn empty() -> Self {
Self::new("") 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. /// Add data to read buffer.
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) { 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 { impl io::Read for TestBuffer {
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> { 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() { if self.err.is_some() {
Err(self.err.take().unwrap()) Err(Rc::try_unwrap(self.err.take().unwrap()).unwrap())
} else { } else {
Err(io::Error::new(io::ErrorKind::WouldBlock, "")) Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
} }
} else { } else {
let size = std::cmp::min(self.read_buf.len(), dst.len()); let size = std::cmp::min(self.read_buf.borrow().len(), dst.len());
let b = self.read_buf.split_to(size); let b = self.read_buf.borrow_mut().split_to(size);
dst[..size].copy_from_slice(&b); dst[..size].copy_from_slice(&b);
Ok(size) Ok(size)
} }
@ -206,7 +242,7 @@ impl io::Read for TestBuffer {
impl io::Write for TestBuffer { impl io::Write for TestBuffer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.write_buf.extend(buf); self.write_buf.borrow_mut().extend(buf);
Ok(buf.len()) Ok(buf.len())
} }

View File

@ -3,9 +3,11 @@ use bitflags::bitflags;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use bytestring::ByteString; use bytestring::ByteString;
use super::frame::Parser; use super::{
use super::proto::{CloseReason, OpCode}; frame::Parser,
use super::ProtocolError; proto::{CloseReason, OpCode},
ProtocolError,
};
/// A WebSocket message. /// A WebSocket message.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -251,7 +253,7 @@ impl Decoder for Codec {
} }
} }
_ => { _ => {
error!("Unfinished fragment {:?}", opcode); log::error!("Unfinished fragment {:?}", opcode);
Err(ProtocolError::ContinuationFragment(opcode)) Err(ProtocolError::ContinuationFragment(opcode))
} }
}; };

View File

@ -1,6 +1,8 @@
use std::future::Future; use std::{
use std::pin::Pin; future::Future,
use std::task::{Context, Poll}; pin::Pin,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_service::{IntoService, Service}; use actix_service::{IntoService, Service};

View File

@ -3,9 +3,11 @@ use std::convert::TryFrom;
use bytes::{Buf, BufMut, BytesMut}; use bytes::{Buf, BufMut, BytesMut};
use log::debug; use log::debug;
use crate::ws::mask::apply_mask; use super::{
use crate::ws::proto::{CloseCode, CloseReason, OpCode}; mask::apply_mask,
use crate::ws::ProtocolError; proto::{CloseCode, CloseReason, OpCode},
ProtocolError,
};
/// A struct representing a WebSocket frame. /// A struct representing a WebSocket frame.
#[derive(Debug)] #[derive(Debug)]

View File

@ -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"; Hello World Hello World Hello World Hello World Hello World";
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_v2() { async fn h1_v2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR))) .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
@ -59,7 +59,7 @@ async fn test_h1_v2() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_connection_close() { async fn connection_close() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR))) .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
@ -73,7 +73,7 @@ async fn test_connection_close() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_with_query_parameter() { async fn with_query_parameter() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|req: Request| async move { .finish(|req: Request| async move {
@ -104,7 +104,7 @@ impl From<ExpectFailed> for Response<BoxBody> {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_expect() { async fn h1_expect() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.expect(|req: Request| async { .expect(|req: Request| async {

View File

@ -1,4 +1,4 @@
use std::io; use std::{io, time::Duration};
use actix_http::{error::Error, HttpService, Response}; use actix_http::{error::Error, HttpService, Response};
use actix_server::Server; use actix_server::Server;
@ -19,7 +19,7 @@ async fn h2_ping_pong() -> io::Result<()> {
.workers(1) .workers(1)
.listen("h2_ping_pong", lst, || { .listen("h2_ping_pong", lst, || {
HttpService::build() HttpService::build()
.keep_alive(3) .keep_alive(Duration::from_secs(3))
.h2(|_| async { Ok::<_, Error>(Response::ok()) }) .h2(|_| async { Ok::<_, Error>(Response::ok()) })
.tcp() .tcp()
})? })?
@ -92,10 +92,10 @@ async fn h2_handshake_timeout() -> io::Result<()> {
.workers(1) .workers(1)
.listen("h2_ping_pong", lst, || { .listen("h2_ping_pong", lst, || {
HttpService::build() HttpService::build()
.keep_alive(30) .keep_alive(Duration::from_secs(30))
// set first request timeout to 5 seconds. // set first request timeout to 5 seconds.
// this is the timeout used for http2 handshake. // this is the timeout used for http2 handshake.
.client_timeout(5000) .client_request_timeout(Duration::from_secs(5))
.h2(|_| async { Ok::<_, Error>(Response::ok()) }) .h2(|_| async { Ok::<_, Error>(Response::ok()) })
.tcp() .tcp()
})? })?

View File

@ -2,7 +2,7 @@ use std::{
convert::Infallible, convert::Infallible,
io::{Read, Write}, io::{Read, Write},
net, thread, net, thread,
time::Duration, time::{Duration, Instant},
}; };
use actix_http::{ use actix_http::{
@ -22,12 +22,12 @@ use futures_util::{
use regex::Regex; use regex::Regex;
#[actix_rt::test] #[actix_rt::test]
async fn test_h1() { async fn h1_basic() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.client_timeout(1000) .client_request_timeout(Duration::from_secs(1))
.client_disconnect(1000) .client_disconnect_timeout(Duration::from_secs(1))
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
ok::<_, Infallible>(Response::ok()) ok::<_, Infallible>(Response::ok())
@ -43,12 +43,12 @@ async fn test_h1() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_2() { async fn h1_2() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.client_timeout(1000) .client_request_timeout(Duration::from_secs(1))
.client_disconnect(1000) .client_disconnect_timeout(Duration::from_secs(1))
.finish(|req: Request| { .finish(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
assert_eq!(req.version(), http::Version::HTTP_11); assert_eq!(req.version(), http::Version::HTTP_11);
@ -75,7 +75,7 @@ impl From<ExpectFailed> for Response<BoxBody> {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_expect_continue() { async fn expect_continue() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.expect(fn_service(|req: Request| { .expect(fn_service(|req: Request| {
@ -106,7 +106,7 @@ async fn test_expect_continue() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_expect_continue_h1() { async fn expect_continue_h1() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.expect(fn_service(|req: Request| { .expect(fn_service(|req: Request| {
@ -139,7 +139,7 @@ async fn test_expect_continue_h1() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_chunked_payload() { async fn chunked_payload() {
let chunk_sizes = vec![32768, 32, 32768]; let chunk_sizes = vec![32768, 32, 32768];
let total_size: usize = chunk_sizes.iter().sum(); let total_size: usize = chunk_sizes.iter().sum();
@ -197,26 +197,43 @@ async fn test_chunked_payload() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_slow_request() { async fn slow_request_408() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.client_timeout(100) .client_request_timeout(Duration::from_millis(200))
.keep_alive(Duration::from_secs(2))
.finish(|_| ok::<_, Infallible>(Response::ok())) .finish(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
let start = Instant::now();
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); 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 mut data = String::new();
let _ = stream.read_to_string(&mut data); 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; srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_malformed_request() { async fn http1_malformed_request() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
@ -234,7 +251,7 @@ async fn test_http1_malformed_request() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive() { async fn http1_keepalive() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
@ -257,23 +274,25 @@ async fn test_http1_keepalive() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive_timeout() { async fn http1_keepalive_timeout() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(1) .keep_alive(Duration::from_secs(1))
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); 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); let _ = stream.read(&mut data);
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
thread::sleep(Duration::from_millis(1100)); 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(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
@ -281,7 +300,7 @@ async fn test_http1_keepalive_timeout() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive_close() { async fn http1_keepalive_close() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
@ -303,7 +322,7 @@ async fn test_http1_keepalive_close() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http10_keepalive_default_close() { async fn http10_keepalive_default_close() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
@ -325,7 +344,7 @@ async fn test_http10_keepalive_default_close() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http10_keepalive() { async fn http10_keepalive() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
@ -354,7 +373,7 @@ async fn test_http10_keepalive() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive_disabled() { async fn http1_keepalive_disabled() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
@ -377,7 +396,7 @@ async fn test_http1_keepalive_disabled() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_content_length() { async fn content_length() {
use actix_http::{ use actix_http::{
header::{HeaderName, HeaderValue}, header::{HeaderName, HeaderValue},
StatusCode, StatusCode,
@ -426,7 +445,7 @@ async fn test_content_length() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_headers() { async fn h1_headers() {
let data = STR.repeat(10); let data = STR.repeat(10);
let data2 = data.clone(); 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"; Hello World Hello World Hello World Hello World Hello World";
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_body() { async fn h1_body() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
@ -511,7 +530,7 @@ async fn test_h1_body() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_head_empty() { async fn h1_head_empty() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
@ -538,7 +557,7 @@ async fn test_h1_head_empty() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_head_binary() { async fn h1_head_binary() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
@ -565,7 +584,7 @@ async fn test_h1_head_binary() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_head_binary2() { async fn h1_head_binary2() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
@ -588,7 +607,7 @@ async fn test_h1_head_binary2() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_body_length() { async fn h1_body_length() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| { .h1(|_| {
@ -612,7 +631,7 @@ async fn test_h1_body_length() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_body_chunked_explicit() { async fn h1_body_chunked_explicit() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| { .h1(|_| {
@ -649,7 +668,7 @@ async fn test_h1_body_chunked_explicit() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_body_chunked_implicit() { async fn h1_body_chunked_implicit() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| { .h1(|_| {
@ -680,7 +699,7 @@ async fn test_h1_body_chunked_implicit() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_response_http_error_handling() { async fn h1_response_http_error_handling() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(fn_service(|_| { .h1(fn_service(|_| {
@ -719,7 +738,7 @@ impl From<BadRequest> for Response<BoxBody> {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_service_error() { async fn h1_service_error() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| err::<Response<()>, _>(BadRequest)) .h1(|_| err::<Response<()>, _>(BadRequest))
@ -738,7 +757,7 @@ async fn test_h1_service_error() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_on_connect() { async fn h1_on_connect() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.on_connect_ext(|_, data| { .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. /// Tests compliance with 304 Not Modified spec in RFC 7232 §4.1.
/// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 /// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
#[actix_rt::test] #[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 // 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 // dispatcher or the client, though similar hangs occur on other tests in this file, only
// succeeding, it seems, because of the keepalive timer // succeeding, it seems, because of the keepalive timer

View File

@ -109,7 +109,7 @@ async fn service(msg: Frame) -> Result<Message, Error> {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_simple() { async fn simple() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.upgrade(fn_factory(|| async { .upgrade(fn_factory(|| async {

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## 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 ## 0.4.0-beta.12 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.12" version = "0.4.0-beta.13"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Multipart form support for Actix Web" description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@ -15,7 +15,7 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.19", default-features = false } actix-web = { version = "4.0.0-rc.1", default-features = false }
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
@ -28,7 +28,7 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-http = "3.0.0-beta.18" actix-http = "3.0.0-rc.1"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.8.4", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

@ -3,11 +3,11 @@
> Multipart form support for Actix Web. > Multipart form support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.12)](https://docs.rs/actix-multipart/0.4.0-beta.12) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.13)](https://docs.rs/actix-multipart/0.4.0-beta.13)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.12/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.12) [![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.13/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.13)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -3,6 +3,32 @@
## Unreleased - 2021-xx-xx ## 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]
[#2590]: https://github.com/actix/actix-web/pull/2590
## 0.5.0-rc.1 - 2022-01-14
- `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568]
- `Resource` is now implemented for `&mut Path<_>` and `RefMut<Path<_>>`. [#2568]
[#2568]: https://github.com/actix/actix-web/pull/2568
## 0.5.0-beta.4 - 2022-01-04 ## 0.5.0-beta.4 - 2022-01-04
- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566] - `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566]
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-router" name = "actix-router"
version = "0.5.0-beta.4" version = "0.5.0-rc.3"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>", "Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde::de::{self, Deserializer, Error as DeError, Visitor}; use serde::de::{self, Deserializer, Error as DeError, Visitor};
use serde::forward_to_deserialize_any; use serde::forward_to_deserialize_any;
@ -20,7 +22,7 @@ macro_rules! unsupported_type {
} }
macro_rules! parse_single_value { macro_rules! parse_single_value {
($trait_fn:ident, $visit_fn:ident, $tp:expr) => { ($trait_fn:ident) => {
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error> fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where where
V: Visitor<'de>, V: Visitor<'de>,
@ -34,15 +36,10 @@ macro_rules! parse_single_value {
.as_str(), .as_str(),
)) ))
} else { } else {
let decoded = FULL_QUOTER Value {
.with(|q| q.requote(self.path[0].as_bytes())) value: &self.path[0],
.unwrap_or_else(|| self.path[0].to_owned()); }
.$trait_fn(visitor)
let v = decoded.parse().map_err(|_| {
de::Error::custom(format!("can not parse {:?} to a {}", &self.path[0], $tp))
})?;
visitor.$visit_fn(v)
} }
} }
}; };
@ -55,8 +52,9 @@ macro_rules! parse_value {
V: Visitor<'de>, V: Visitor<'de>,
{ {
let decoded = FULL_QUOTER let decoded = FULL_QUOTER
.with(|q| q.requote(self.value.as_bytes())) .with(|q| q.requote_str_lossy(self.value))
.unwrap_or_else(|| self.value.to_owned()); .map(Cow::Owned)
.unwrap_or(Cow::Borrowed(self.value));
let v = decoded.parse().map_err(|_| { let v = decoded.parse().map_err(|_| {
de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp)) de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp))
@ -204,26 +202,26 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T>
} }
unsupported_type!(deserialize_any, "'any'"); unsupported_type!(deserialize_any, "'any'");
unsupported_type!(deserialize_bytes, "bytes");
unsupported_type!(deserialize_option, "Option<T>"); unsupported_type!(deserialize_option, "Option<T>");
unsupported_type!(deserialize_identifier, "identifier"); unsupported_type!(deserialize_identifier, "identifier");
unsupported_type!(deserialize_ignored_any, "ignored_any"); unsupported_type!(deserialize_ignored_any, "ignored_any");
parse_single_value!(deserialize_bool, visit_bool, "bool"); parse_single_value!(deserialize_bool);
parse_single_value!(deserialize_i8, visit_i8, "i8"); parse_single_value!(deserialize_i8);
parse_single_value!(deserialize_i16, visit_i16, "i16"); parse_single_value!(deserialize_i16);
parse_single_value!(deserialize_i32, visit_i32, "i32"); parse_single_value!(deserialize_i32);
parse_single_value!(deserialize_i64, visit_i64, "i64"); parse_single_value!(deserialize_i64);
parse_single_value!(deserialize_u8, visit_u8, "u8"); parse_single_value!(deserialize_u8);
parse_single_value!(deserialize_u16, visit_u16, "u16"); parse_single_value!(deserialize_u16);
parse_single_value!(deserialize_u32, visit_u32, "u32"); parse_single_value!(deserialize_u32);
parse_single_value!(deserialize_u64, visit_u64, "u64"); parse_single_value!(deserialize_u64);
parse_single_value!(deserialize_f32, visit_f32, "f32"); parse_single_value!(deserialize_f32);
parse_single_value!(deserialize_f64, visit_f64, "f64"); parse_single_value!(deserialize_f64);
parse_single_value!(deserialize_str, visit_string, "String"); parse_single_value!(deserialize_str);
parse_single_value!(deserialize_string, visit_string, "String"); parse_single_value!(deserialize_string);
parse_single_value!(deserialize_byte_buf, visit_string, "String"); parse_single_value!(deserialize_bytes);
parse_single_value!(deserialize_char, visit_char, "char"); parse_single_value!(deserialize_byte_buf);
parse_single_value!(deserialize_char);
} }
struct ParamsDeserializer<'de, T: ResourcePath> { struct ParamsDeserializer<'de, T: ResourcePath> {
@ -303,8 +301,6 @@ impl<'de> Deserializer<'de> for Value<'de> {
parse_value!(deserialize_u64, visit_u64, "u64"); parse_value!(deserialize_u64, visit_u64, "u64");
parse_value!(deserialize_f32, visit_f32, "f32"); parse_value!(deserialize_f32, visit_f32, "f32");
parse_value!(deserialize_f64, visit_f64, "f64"); parse_value!(deserialize_f64, visit_f64, "f64");
parse_value!(deserialize_string, visit_string, "String");
parse_value!(deserialize_byte_buf, visit_string, "String");
parse_value!(deserialize_char, visit_char, "char"); parse_value!(deserialize_char, visit_char, "char");
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error> fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@ -332,18 +328,38 @@ impl<'de> Deserializer<'de> for Value<'de> {
visitor.visit_unit() visitor.visit_unit()
} }
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_borrowed_bytes(self.value.as_bytes())
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error> fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where where
V: Visitor<'de>, V: Visitor<'de>,
{ {
visitor.visit_borrowed_str(self.value) match FULL_QUOTER.with(|q| q.requote_str_lossy(self.value)) {
Some(s) => visitor.visit_string(s),
None => visitor.visit_borrowed_str(self.value),
}
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
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()),
}
}
fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_bytes(visitor)
}
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_str(visitor)
} }
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error> fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@ -671,12 +687,12 @@ mod tests {
fn deserialize_path_decode_seq() { fn deserialize_path_decode_seq() {
let rdef = ResourceDef::new("/{key}/{value}"); let rdef = ResourceDef::new("/{key}/{value}");
let mut path = Path::new("/%25/%2F"); let mut path = Path::new("/%30%25/%30%2F");
rdef.capture_match_info(&mut path); rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path); let de = PathDeserializer::new(&path);
let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap(); let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(segment.0, "%"); assert_eq!(segment.0, "0%");
assert_eq!(segment.1, "/"); assert_eq!(segment.1, "0/");
} }
#[test] #[test]
@ -697,6 +713,32 @@ mod tests {
assert_eq!(vals.value, "/"); assert_eq!(vals.value, "/");
} }
#[test]
fn deserialize_borrowed() {
#[derive(Debug, Deserialize)]
struct Params<'a> {
val: &'a str,
}
let rdef = ResourceDef::new("/{val}");
let mut path = Path::new("/X");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
let params: Params<'_> = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(params.val, "X");
let de = PathDeserializer::new(&path);
let params: &str = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(params, "X");
let mut path = Path::new("/%2F");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
assert!(<Params<'_> as serde::Deserialize>::deserialize(de).is_err());
let de = PathDeserializer::new(&path);
assert!(<&str as serde::Deserialize>::deserialize(de).is_err());
}
// #[test] // #[test]
// fn test_extract_path_decode() { // fn test_extract_path_decode() {
// let mut router = Router::<()>::default(); // let mut router = Router::<()>::default();

View File

@ -22,7 +22,7 @@ pub use self::pattern::{IntoPatterns, Patterns};
pub use self::quoter::Quoter; pub use self::quoter::Quoter;
pub use self::resource::ResourceDef; pub use self::resource::ResourceDef;
pub use self::resource_path::{Resource, ResourcePath}; pub use self::resource_path::{Resource, ResourcePath};
pub use self::router::{ResourceInfo, Router, RouterBuilder}; pub use self::router::{ResourceId, Router, RouterBuilder};
#[cfg(feature = "http")] #[cfg(feature = "http")]
pub use self::url::Url; pub use self::url::Url;

View File

@ -1,5 +1,5 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::ops::Index; use std::ops::{DerefMut, Index};
use firestorm::profile_method; use firestorm::profile_method;
use serde::de; use serde::de;
@ -37,19 +37,39 @@ impl<T: ResourcePath> Path<T> {
} }
} }
/// Get reference to inner path instance. /// Returns reference to inner path instance.
#[inline] #[inline]
pub fn get_ref(&self) -> &T { pub fn get_ref(&self) -> &T {
&self.path &self.path
} }
/// Get mutable reference to inner path instance. /// Returns mutable reference to inner path instance.
#[inline] #[inline]
pub fn get_mut(&mut self) -> &mut T { pub fn get_mut(&mut self) -> &mut T {
&mut self.path &mut self.path
} }
/// Path. /// Returns full path as a string.
#[inline]
pub fn as_str(&self) -> &str {
profile_method!(as_str);
self.path.path()
}
/// Returns unprocessed part of the path.
///
/// Returns empty string if no more is to be processed.
#[inline]
pub fn unprocessed(&self) -> &str {
profile_method!(unprocessed);
// clamp skip to path length
let skip = (self.skip as usize).min(self.as_str().len());
&self.path.path()[skip..]
}
/// Returns unprocessed part of the path.
#[doc(hidden)]
#[deprecated(since = "0.6.0", note = "Use `.as_str()` or `.unprocessed()`.")]
#[inline] #[inline]
pub fn path(&self) -> &str { pub fn path(&self) -> &str {
profile_method!(path); profile_method!(path);
@ -66,6 +86,8 @@ impl<T: ResourcePath> Path<T> {
/// Set new path. /// Set new path.
#[inline] #[inline]
pub fn set(&mut self, path: T) { pub fn set(&mut self, path: T) {
profile_method!(set);
self.skip = 0; self.skip = 0;
self.path = path; self.path = path;
self.segments.clear(); self.segments.clear();
@ -74,6 +96,8 @@ impl<T: ResourcePath> Path<T> {
/// Reset state. /// Reset state.
#[inline] #[inline]
pub fn reset(&mut self) { pub fn reset(&mut self) {
profile_method!(reset);
self.skip = 0; self.skip = 0;
self.segments.clear(); self.segments.clear();
} }
@ -81,6 +105,7 @@ impl<T: ResourcePath> Path<T> {
/// Skip first `n` chars in path. /// Skip first `n` chars in path.
#[inline] #[inline]
pub fn skip(&mut self, n: u16) { pub fn skip(&mut self, n: u16) {
profile_method!(skip);
self.skip += n; self.skip += n;
} }
@ -102,6 +127,8 @@ impl<T: ResourcePath> Path<T> {
name: impl Into<Cow<'static, str>>, name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>, value: impl Into<Cow<'static, str>>,
) { ) {
profile_method!(add_static);
self.segments self.segments
.push((name.into(), PathItem::Static(value.into()))); .push((name.into(), PathItem::Static(value.into())));
} }
@ -136,11 +163,6 @@ impl<T: ResourcePath> Path<T> {
None None
} }
/// Get unprocessed part of the path
pub fn unprocessed(&self) -> &str {
&self.path.path()[(self.skip as usize)..]
}
/// Get matched parameter by name. /// Get matched parameter by name.
/// ///
/// If keyed parameter is not available empty string is used as default value. /// If keyed parameter is not available empty string is used as default value.
@ -213,8 +235,38 @@ impl<T: ResourcePath> Index<usize> for Path<T> {
} }
} }
impl<T: ResourcePath> Resource<T> for Path<T> { impl<T: ResourcePath> Resource for Path<T> {
fn resource_path(&mut self) -> &mut Self { type Path = T;
fn resource_path(&mut self) -> &mut Path<Self::Path> {
self self
} }
} }
impl<T, P> Resource for T
where
T: DerefMut<Target = Path<P>>,
P: ResourcePath,
{
type Path = P;
fn resource_path(&mut self) -> &mut Path<Self::Path> {
&mut *self
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
#[test]
fn deref_impls() {
let mut foo = Path::new("/foo");
let _ = (&mut foo).resource_path();
let foo = RefCell::new(foo);
let _ = foo.borrow_mut().resource_path();
}
}

View File

@ -64,10 +64,15 @@ impl Quoter {
quoter quoter
} }
/// Re-quotes... ? /// Decodes safe percent-encoded sequences from `val`.
/// ///
/// Returns `None` when no modification to the original string was required. /// Returns `None` when no modification to the original byte string was required.
pub fn requote(&self, val: &[u8]) -> Option<String> { ///
/// 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 has_pct = 0;
let mut pct = [b'%', 0, 0]; let mut pct = [b'%', 0, 0];
let mut idx = 0; let mut idx = 0;
@ -121,7 +126,12 @@ impl Quoter {
idx += 1; 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] #[test]
fn custom_quoter() { fn custom_quoter() {
let q = Quoter::new(b"", b"+"); let q = Quoter::new(b"", b"+");
assert_eq!(q.requote(b"/a%25c").unwrap(), "/a%c"); assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
assert_eq!(q.requote(b"/a%2Bc").unwrap(), "/a%2Bc"); assert_eq!(q.requote(b"/a%2Bc").unwrap(), b"/a%2Bc");
let q = Quoter::new(b"%+", b"/"); let q = Quoter::new(b"%+", b"/");
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c"); assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb"); assert_eq!(q.requote(b"/a%2fb").unwrap(), b"/a%2fb");
assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb"); assert_eq!(q.requote(b"/a%2Fb").unwrap(), b"/a%2Fb");
assert_eq!(q.requote(b"/a%0Ab").unwrap(), "/a\nb"); 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] #[test]

View File

@ -8,10 +8,7 @@ use std::{
use firestorm::{profile_fn, profile_method, profile_section}; use firestorm::{profile_fn, profile_method, profile_section};
use regex::{escape, Regex, RegexSet}; use regex::{escape, Regex, RegexSet};
use crate::{ use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
path::{Path, PathItem},
IntoPatterns, Patterns, Resource, ResourcePath,
};
const MAX_DYNAMIC_SEGMENTS: usize = 16; 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. /// 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.get("path").unwrap(), "HEAD/Cargo.toml");
/// assert_eq!(path.unprocessed(), ""); /// 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); 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 /// Collects dynamic segment values into `resource` after matching paths and executing
@ -655,13 +652,12 @@ impl ResourceDef {
/// use actix_router::{Path, ResourceDef}; /// use actix_router::{Path, ResourceDef};
/// ///
/// fn try_match(resource: &ResourceDef, path: &mut Path<&str>) -> bool { /// 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( /// resource.capture_match_info_fn(
/// path, /// path,
/// // when env var is not set, reject when path contains "admin" /// // when env var is not set, reject when path contains "admin"
/// |res, admin_allowed| !res.path().contains("admin"), /// |res| !(!admin_allowed && res.path().contains("admin")),
/// &admin_allowed
/// ) /// )
/// } /// }
/// ///
@ -678,22 +674,16 @@ impl ResourceDef {
/// assert!(!try_match(&resource, &mut path)); /// assert!(!try_match(&resource, &mut path));
/// assert_eq!(path.unprocessed(), "/user/admin/stars"); /// assert_eq!(path.unprocessed(), "/user/admin/stars");
/// ``` /// ```
pub fn capture_match_info_fn<R, T, F, U>( pub fn capture_match_info_fn<R, F>(&self, resource: &mut R, check_fn: F) -> bool
&self,
resource: &mut R,
check_fn: F,
user_data: U,
) -> bool
where where
R: Resource<T>, R: Resource,
T: ResourcePath, F: FnOnce(&R) -> bool,
F: FnOnce(&R, U) -> bool,
{ {
profile_method!(capture_match_info_fn); profile_method!(capture_match_info_fn);
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default(); let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
let path = resource.resource_path(); let path = resource.resource_path();
let path_str = path.path(); let path_str = path.unprocessed();
let (matched_len, matched_vars) = match &self.pat_type { let (matched_len, matched_vars) = match &self.pat_type {
PatternType::Static(pattern) => { PatternType::Static(pattern) => {
@ -711,7 +701,7 @@ impl ResourceDef {
let captures = { let captures = {
profile_section!(pattern_dynamic_regex_exec); profile_section!(pattern_dynamic_regex_exec);
match re.captures(path.path()) { match re.captures(path.unprocessed()) {
Some(captures) => captures, Some(captures) => captures,
_ => return false, _ => return false,
} }
@ -739,7 +729,7 @@ impl ResourceDef {
PatternType::DynamicSet(re, params) => { PatternType::DynamicSet(re, params) => {
profile_section!(pattern_dynamic_set); profile_section!(pattern_dynamic_set);
let path = path.path(); let path = path.unprocessed();
let (pattern, names) = match re.matches(path).into_iter().next() { let (pattern, names) = match re.matches(path).into_iter().next() {
Some(idx) => &params[idx], Some(idx) => &params[idx],
_ => return false, _ => return false,
@ -763,7 +753,7 @@ impl ResourceDef {
} }
}; };
if !check_fn(resource, user_data) { if !check_fn(resource) {
return false; return false;
} }
@ -858,7 +848,7 @@ impl ResourceDef {
S: BuildHasher, S: BuildHasher,
{ {
profile_method!(resource_path_from_map); 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`. /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`.
@ -1158,6 +1148,7 @@ pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::Path;
#[test] #[test]
fn equivalence() { fn equivalence() {

View File

@ -2,8 +2,11 @@ use crate::Path;
// TODO: this trait is necessary, document it // TODO: this trait is necessary, document it
// see impl Resource for ServiceRequest // see impl Resource for ServiceRequest
pub trait Resource<T: ResourcePath> { pub trait Resource {
fn resource_path(&mut self) -> &mut Path<T>; /// Type of resource's path returned in `resource_path`.
type Path: ResourcePath;
fn resource_path(&mut self) -> &mut Path<Self::Path>;
} }
pub trait ResourcePath { pub trait ResourcePath {

View File

@ -1,95 +1,87 @@
use firestorm::profile_method; use firestorm::profile_method;
use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; use crate::{IntoPatterns, Resource, ResourceDef};
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct ResourceId(pub u16); pub struct ResourceId(pub u16);
/// Information about current resource
#[derive(Debug, Clone)]
pub struct ResourceInfo {
#[allow(dead_code)]
resource: ResourceId,
}
/// Resource router. /// 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 = ()> { pub struct Router<T, U = ()> {
routes: Vec<(ResourceDef, T, Option<U>)>, routes: Vec<(ResourceDef, T, U)>,
} }
impl<T, U> Router<T, U> { impl<T, U> Router<T, U> {
/// Constructs new `RouterBuilder` with empty route list.
pub fn build() -> RouterBuilder<T, U> { pub fn build() -> RouterBuilder<T, U> {
RouterBuilder { RouterBuilder { routes: Vec::new() }
resources: Vec::new(),
}
} }
pub fn recognize<R, P>(&self, resource: &mut R) -> Option<(&T, ResourceId)> /// 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 where
R: Resource<P>, R: Resource,
P: ResourcePath,
{ {
profile_method!(recognize); profile_method!(recognize);
self.recognize_fn(resource, |_, _| true)
for item in self.routes.iter() {
if item.0.capture_match_info(resource.resource_path()) {
return Some((&item.1, ResourceId(item.0.id())));
}
} }
None /// 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)>
pub fn recognize_mut<R, P>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
where where
R: Resource<P>, R: Resource,
P: ResourcePath,
{ {
profile_method!(recognize_mut); profile_method!(recognize_mut);
self.recognize_mut_fn(resource, |_, _| true)
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 /// Finds the value in the router that matches a given [routing resource](Resource) and passes
} /// an additional predicate check using context data.
///
pub fn recognize_fn<R, P, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)> /// 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 where
F: Fn(&R, &Option<U>) -> bool, R: Resource,
R: Resource<P>, F: FnMut(&R, &U) -> bool,
P: ResourcePath,
{ {
profile_method!(recognize_checked); profile_method!(recognize_checked);
for item in self.routes.iter() { for (rdef, val, ctx) in self.routes.iter() {
if item.0.capture_match_info_fn(resource, &check, &item.2) { if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
return Some((&item.1, ResourceId(item.0.id()))); return Some((val, ResourceId(rdef.id())));
} }
} }
None None
} }
pub fn recognize_mut_fn<R, P, F>( /// 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, &mut self,
resource: &mut R, resource: &mut R,
check: F, mut check: F,
) -> Option<(&mut T, ResourceId)> ) -> Option<(&mut T, ResourceId)>
where where
F: Fn(&R, &Option<U>) -> bool, R: Resource,
R: Resource<P>, F: FnMut(&R, &U) -> bool,
P: ResourcePath,
{ {
profile_method!(recognize_mut_checked); profile_method!(recognize_mut_checked);
for item in self.routes.iter_mut() { for (rdef, val, ctx) in self.routes.iter_mut() {
if item.0.capture_match_info_fn(resource, &check, &item.2) { if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
return Some((&mut item.1, ResourceId(item.0.id()))); return Some((val, ResourceId(rdef.id())));
} }
} }
@ -97,49 +89,69 @@ impl<T, U> Router<T, U> {
} }
} }
/// Builder for an ordered [routing](Router) list.
pub struct RouterBuilder<T, U = ()> { pub struct RouterBuilder<T, U = ()> {
resources: Vec<(ResourceDef, T, Option<U>)>, routes: Vec<(ResourceDef, T, U)>,
} }
impl<T, U> RouterBuilder<T, U> { impl<T, U> RouterBuilder<T, U> {
/// Register resource for specified path. /// Adds a new route to the end of the routing list.
pub fn path<P: IntoPatterns>( ///
/// Returns mutable references to elements of the new route.
pub fn push(
&mut self, &mut self,
path: P, rdef: ResourceDef,
resource: T, val: T,
) -> &mut (ResourceDef, T, Option<U>) { ctx: U,
profile_method!(path); ) -> (&mut ResourceDef, &mut T, &mut U) {
profile_method!(push);
self.resources self.routes.push((rdef, val, ctx));
.push((ResourceDef::new(path), resource, None)); self.routes
self.resources.last_mut().unwrap() .last_mut()
} .map(|(rdef, val, ctx)| (rdef, val, ctx))
.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()
} }
/// Finish configuration and create router instance. /// Finish configuration and create router instance.
pub fn finish(self) -> Router<T, U> { pub fn finish(self) -> Router<T, U> {
Router { 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)] #[cfg(test)]
mod tests { mod tests {
use crate::path::Path; use crate::path::Path;
@ -260,6 +272,7 @@ mod tests {
router.path("/name/{val}", 11); router.path("/name/{val}", 11);
let mut router = router.finish(); let mut router = router.finish();
// test skip beyond path length
let mut path = Path::new("/name"); let mut path = Path::new("/name");
path.skip(6); path.skip(6);
assert!(router.recognize_mut(&mut path).is_none()); assert!(router.recognize_mut(&mut path).is_none());

View File

@ -15,14 +15,14 @@ pub struct Url {
impl Url { impl Url {
#[inline] #[inline]
pub fn new(uri: http::Uri) -> Url { 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 } Url { uri, path }
} }
#[inline] #[inline]
pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
Url { Url {
path: quoter.requote(uri.path().as_bytes()), path: quoter.requote_str_lossy(uri.path()),
uri, uri,
} }
} }
@ -45,13 +45,13 @@ impl Url {
#[inline] #[inline]
pub fn update(&mut self, uri: &http::Uri) { pub fn update(&mut self, uri: &http::Uri) {
self.uri = uri.clone(); 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] #[inline]
pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) { pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) {
self.uri = uri.clone(); self.uri = uri.clone();
self.path = quoter.requote(uri.path().as_bytes()); self.path = quoter.requote_str_lossy(uri.path());
} }
} }
@ -121,7 +121,7 @@ mod tests {
} }
#[test] #[test]
fn valid_utf8_multibyte() { fn valid_utf8_multi_byte() {
let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>(); let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>();
let encoded = percent_encode(test.as_bytes()); let encoded = percent_encode(test.as_bytes());
let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded)); let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded));
@ -135,6 +135,6 @@ mod tests {
let path = Path::new(Url::new(uri)); let path = Path::new(Url::new(uri));
// We should always get a valid utf8 string // We should always get a valid utf8 string
assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok()); assert!(String::from_utf8(path.as_str().as_bytes().to_owned()).is_ok());
} }
} }

View File

@ -3,6 +3,12 @@
## Unreleased - 2021-xx-xx ## 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 ## 0.1.0-beta.11 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.0-beta.11" version = "0.1.0-beta.12"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.18" actix-http = "3.0.0-rc.1"
actix-http-test = "3.0.0-beta.11" actix-http-test = "3.0.0-beta.12"
actix-rt = "2.1" actix-rt = "2.1"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.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.18", 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-core = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] } futures-util = { version = "0.3.7", default-features = false, features = [] }

View File

@ -149,7 +149,7 @@ where
let local_addr = tcp.local_addr().unwrap(); let local_addr = tcp.local_addr().unwrap();
let factory = factory.clone(); let factory = factory.clone();
let srv_cfg = cfg.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(); let builder = Server::build().workers(1).disable_signals().system_exit();
@ -167,7 +167,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_request_timeout(timeout)
.h1(map_config(fac, move |_| app_cfg.clone())) .h1(map_config(fac, move |_| app_cfg.clone()))
.tcp() .tcp()
}), }),
@ -183,7 +183,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_request_timeout(timeout)
.h2(map_config(fac, move |_| app_cfg.clone())) .h2(map_config(fac, move |_| app_cfg.clone()))
.tcp() .tcp()
}), }),
@ -199,7 +199,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_request_timeout(timeout)
.finish(map_config(fac, move |_| app_cfg.clone())) .finish(map_config(fac, move |_| app_cfg.clone()))
.tcp() .tcp()
}), }),
@ -218,7 +218,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_request_timeout(timeout)
.h1(map_config(fac, move |_| app_cfg.clone())) .h1(map_config(fac, move |_| app_cfg.clone()))
.openssl(acceptor.clone()) .openssl(acceptor.clone())
}), }),
@ -234,7 +234,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_request_timeout(timeout)
.h2(map_config(fac, move |_| app_cfg.clone())) .h2(map_config(fac, move |_| app_cfg.clone()))
.openssl(acceptor.clone()) .openssl(acceptor.clone())
}), }),
@ -250,7 +250,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_request_timeout(timeout)
.finish(map_config(fac, move |_| app_cfg.clone())) .finish(map_config(fac, move |_| app_cfg.clone()))
.openssl(acceptor.clone()) .openssl(acceptor.clone())
}), }),
@ -269,7 +269,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_request_timeout(timeout)
.h1(map_config(fac, move |_| app_cfg.clone())) .h1(map_config(fac, move |_| app_cfg.clone()))
.rustls(config.clone()) .rustls(config.clone())
}), }),
@ -285,7 +285,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_request_timeout(timeout)
.h2(map_config(fac, move |_| app_cfg.clone())) .h2(map_config(fac, move |_| app_cfg.clone()))
.rustls(config.clone()) .rustls(config.clone())
}), }),
@ -301,7 +301,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_request_timeout(timeout)
.finish(map_config(fac, move |_| app_cfg.clone())) .finish(map_config(fac, move |_| app_cfg.clone()))
.rustls(config.clone()) .rustls(config.clone())
}), }),
@ -388,7 +388,7 @@ pub fn config() -> TestServerConfig {
pub struct TestServerConfig { pub struct TestServerConfig {
tp: HttpVer, tp: HttpVer,
stream: StreamType, stream: StreamType,
client_timeout: u64, client_request_timeout: Duration,
} }
impl Default for TestServerConfig { impl Default for TestServerConfig {
@ -403,7 +403,7 @@ impl TestServerConfig {
TestServerConfig { TestServerConfig {
tp: HttpVer::Both, tp: HttpVer::Both,
stream: StreamType::Tcp, stream: StreamType::Tcp,
client_timeout: 5000, client_request_timeout: Duration::from_secs(5),
} }
} }
@ -433,9 +433,9 @@ impl TestServerConfig {
self self
} }
/// Set client timeout in milliseconds for first request. /// Set client timeout for first request.
pub fn client_timeout(mut self, val: u64) -> Self { pub fn client_request_timeout(mut self, dur: Duration) -> Self {
self.client_timeout = val; self.client_request_timeout = dur;
self self
} }
} }

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## 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 ## 4.0.0-beta.10 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-actors" name = "actix-web-actors"
version = "4.0.0-beta.10" version = "4.0.0-beta.11"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for Actix Web" description = "Actix actors support for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@ -16,8 +16,8 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.12.0", default-features = false } actix = { version = "0.12.0", default-features = false }
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.18" actix-http = "3.0.0-rc.1"
actix-web = { version = "4.0.0-beta.19", default-features = false } actix-web = { version = "4.0.0-rc.1", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
@ -27,8 +27,8 @@ tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.11" actix-test = "0.1.0-beta.12"
awc = { version = "3.0.0-beta.18", default-features = false } awc = { version = "3.0.0-beta.20", default-features = false }
env_logger = "0.9" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }

View File

@ -3,11 +3,11 @@
> Actix actors support for Actix Web. > Actix actors support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.10)](https://docs.rs/actix-web-actors/4.0.0-beta.10) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.11)](https://docs.rs/actix-web-actors/4.0.0-beta.11)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-web-actors.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10) [![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.11)
[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -228,8 +228,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_default_resource() { async fn test_default_resource() {
let srv = let srv = init_service(App::new().service(web::resource("/test").to(|| async {
init_service(App::new().service(web::resource("/test").to(|| {
HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 })) HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 }))
}))) })))
.await; .await;

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## 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 ## 0.5.0-rc.1 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "0.5.0-rc.1" version = "0.5.0-rc.2"
description = "Routing and runtime macros for Actix Web" description = "Routing and runtime macros for Actix Web"
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web.git"
@ -15,7 +15,7 @@ edition = "2018"
proc-macro = true proc-macro = true
[dependencies] [dependencies]
actix-router = "0.5.0-beta.4" actix-router = "0.5.0-rc.3"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "1", features = ["full", "parsing"] } syn = { version = "1", features = ["full", "parsing"] }
@ -23,9 +23,9 @@ syn = { version = "1", features = ["full", "parsing"] }
[dev-dependencies] [dev-dependencies]
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.11" actix-test = "0.1.0-beta.12"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = "4.0.0-beta.19" actix-web = "4.0.0-rc.1"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
trybuild = "1" trybuild = "1"

View File

@ -3,11 +3,11 @@
> Routing and runtime macros for Actix Web. > Routing and runtime macros for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-rc.1)](https://docs.rs/actix-web-codegen/0.5.0-rc.1) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-rc.2)](https://docs.rs/actix-web-codegen/0.5.0-rc.2)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-web-codegen.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1) [![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.2/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.2)
[![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -39,13 +39,18 @@
//! ``` //! ```
//! # use actix_web::HttpResponse; //! # use actix_web::HttpResponse;
//! # use actix_web_codegen::route; //! # use actix_web_codegen::route;
//! #[route("/test", method="GET", method="HEAD")] //! #[route("/test", method = "GET", method = "HEAD")]
//! async fn get_and_head_handler() -> HttpResponse { //! async fn get_and_head_handler() -> HttpResponse {
//! HttpResponse::Ok().finish() //! HttpResponse::Ok().finish()
//! } //! }
//! ``` //! ```
//! //!
//! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes //! # Multiple Path Handlers
//! There are no macros to generate multi-path handlers. Let us know in [this issue].
//!
//! [this issue]: https://github.com/actix/actix-web/issues/1709
//!
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
//! [GET]: macro@get //! [GET]: macro@get
//! [POST]: macro@post //! [POST]: macro@post
//! [PUT]: macro@put //! [PUT]: macro@put
@ -73,22 +78,23 @@ mod route;
/// ``` /// ```
/// ///
/// # Attributes /// # Attributes
/// - `"path"` - Raw literal string with path for which to register handler. /// - `"path"`: Raw literal string with path for which to register handler.
/// - `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used. /// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function
/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example. /// name of handler is used.
/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` /// - `method = "HTTP_METHOD"`: Registers HTTP method to provide guard for. Upper-case string,
/// - `wrap="Middleware"` - Registers a resource middleware. /// "GET", "POST" for example.
/// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`.
/// - `wrap = "Middleware"`: Registers a resource middleware.
/// ///
/// # Notes /// # Notes
/// Function name can be specified as any expression that is going to be accessible to the generate /// Function name can be specified as any expression that is going to be accessible to the generate
/// code, e.g `my_guard` or `my_module::my_guard`. /// code, e.g `my_guard` or `my_module::my_guard`.
/// ///
/// # Example /// # Examples
///
/// ``` /// ```
/// # use actix_web::HttpResponse; /// # use actix_web::HttpResponse;
/// # use actix_web_codegen::route; /// # use actix_web_codegen::route;
/// #[route("/test", method="GET", method="HEAD")] /// #[route("/test", method = "GET", method = "HEAD")]
/// async fn example() -> HttpResponse { /// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish() /// HttpResponse::Ok().finish()
/// } /// }
@ -98,69 +104,54 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(None, args, input) route::with_method(None, args, input)
} }
macro_rules! doc_comment {
($x:expr; $($tt:tt)*) => {
#[doc = $x]
$($tt)*
};
}
macro_rules! method_macro { macro_rules! method_macro {
( ($variant:ident, $method:ident) => {
$($variant:ident, $method:ident,)+ #[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]
) => { ///
$(doc_comment! { /// # Syntax
concat!(" /// ```plain
Creates route handler with `actix_web::guard::", stringify!($variant), "`. #[doc = concat!("#[", stringify!($method), r#"("path"[, attributes])]"#)]
/// ```
# Syntax ///
```plain /// # Attributes
#[", stringify!($method), r#"("path"[, attributes])] /// - `"path"`: Raw literal string with path for which to register handler.
``` /// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function
/// name of handler is used.
# Attributes /// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`.
- `"path"` - Raw literal string with path for which to register handler. /// - `wrap = "Middleware"`: Registers a resource middleware.
- `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used. ///
- `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`. /// # Notes
- `wrap="Middleware"` - Registers a resource middleware. /// Function name can be specified as any expression that is going to be accessible to the
/// generate code, e.g `my_guard` or `my_module::my_guard`.
# Notes ///
Function name can be specified as any expression that is going to be accessible to the generate /// # Examples
code, e.g `my_guard` or `my_module::my_guard`. /// ```
/// # use actix_web::HttpResponse;
# Example #[doc = concat!("# use actix_web_codegen::", stringify!($method), ";")]
#[doc = concat!("#[", stringify!($method), r#"("/")]"#)]
``` /// async fn example() -> HttpResponse {
# use actix_web::HttpResponse; /// HttpResponse::Ok().finish()
# use actix_web_codegen::"#, stringify!($method), "; /// }
#[", stringify!($method), r#"("/")] /// ```
async fn example() -> HttpResponse { #[proc_macro_attribute]
HttpResponse::Ok().finish() pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
}
```
"#);
#[proc_macro_attribute]
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(Some(route::MethodType::$variant), args, input) route::with_method(Some(route::MethodType::$variant), args, input)
} }
})+
}; };
} }
method_macro! { method_macro!(Get, get);
Get, get, method_macro!(Post, post);
Post, post, method_macro!(Put, put);
Put, put, method_macro!(Delete, delete);
Delete, delete, method_macro!(Head, head);
Head, head, method_macro!(Connect, connect);
Connect, connect, method_macro!(Options, options);
Options, options, method_macro!(Trace, trace);
Trace, trace, method_macro!(Patch, patch);
Patch, patch,
}
/// Marks async main function as the actix system entry-point.
/// Marks async main function as the Actix Web system entry-point.
///
/// # Examples /// # Examples
/// ``` /// ```
/// #[actix_web::main] /// #[actix_web::main]

View File

@ -302,13 +302,13 @@ impl ToTokens for Route {
if methods.len() > 1 { if methods.len() > 1 {
quote! { quote! {
.guard( .guard(
actix_web::guard::Any(actix_web::guard::#first()) ::actix_web::guard::Any(::actix_web::guard::#first())
#(.or(actix_web::guard::#others()))* #(.or(::actix_web::guard::#others()))*
) )
} }
} else { } else {
quote! { quote! {
.guard(actix_web::guard::#first()) .guard(::actix_web::guard::#first())
} }
} }
}; };
@ -318,17 +318,17 @@ impl ToTokens for Route {
#[allow(non_camel_case_types, missing_docs)] #[allow(non_camel_case_types, missing_docs)]
pub struct #name; pub struct #name;
impl actix_web::dev::HttpServiceFactory for #name { impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) { fn register(self, __config: &mut actix_web::dev::AppService) {
#ast #ast
let __resource = actix_web::Resource::new(#path) let __resource = ::actix_web::Resource::new(#path)
.name(#resource_name) .name(#resource_name)
#method_guards #method_guards
#(.guard(actix_web::guard::fn_guard(#guards)))* #(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))* #(.wrap(#wrappers))*
.#resource_type(#name); .#resource_type(#name);
actix_web::dev::HttpServiceFactory::register(__resource, __config) ::actix_web::dev::HttpServiceFactory::register(__resource, __config)
} }
} }
}; };

View File

@ -3,6 +3,14 @@
## Unreleased - 2021-xx-xx ## 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`.
## 3.0.0-beta.18 - 2022-01-04 ## 3.0.0-beta.18 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.0-beta.18" version = "3.0.0-beta.20"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@ -60,7 +60,7 @@ dangerous-h2c = []
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-http = "3.0.0-beta.18" actix-http = { version = "3.0.0-rc.1", features = ["http2", "ws"] }
actix-rt = { version = "2.1", default-features = false } actix-rt = { version = "2.1", default-features = false }
actix-tls = { version = "3.0.0", features = ["connect", "uri"] } actix-tls = { version = "3.0.0", features = ["connect", "uri"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
@ -93,15 +93,15 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
trust-dns-resolver = { version = "0.20.0", optional = true } trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http = { version = "3.0.0-beta.18", features = ["openssl"] } actix-http = { version = "3.0.0-rc.1", features = ["openssl"] }
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.0.0-rc.2" 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-tls = { version = "3.0.0", features = ["openssl", "rustls"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.19", features = ["openssl"] } actix-web = { version = "4.0.0-rc.1", features = ["openssl"] }
brotli2 = "0.3.2" brotli = "3.3.3"
const-str = "0.3" const-str = "0.3"
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
@ -109,6 +109,7 @@ futures-util = { version = "0.3.7", default-features = false }
static_assertions = "1.1" static_assertions = "1.1"
rcgen = "0.8" rcgen = "0.8"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
zstd = "0.9" zstd = "0.9"
[[example]] [[example]]

View File

@ -3,9 +3,9 @@
> Async HTTP and WebSocket client library. > Async HTTP and WebSocket client library.
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.18)](https://docs.rs/awc/3.0.0-beta.18) [![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.20)](https://docs.rs/awc/3.0.0-beta.20)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.18/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.18) [![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.20/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.20)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources

View File

@ -1,13 +1,13 @@
use std::error::Error as StdError; use std::error::Error as StdError;
#[actix_web::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn StdError>> { async fn main() -> Result<(), Box<dyn StdError>> {
std::env::set_var("RUST_LOG", "client=trace,awc=trace,actix_http=trace"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
// construct request builder
let client = awc::Client::new(); let client = awc::Client::new();
// Create request builder, configure request and send // configure request
let request = client let request = client
.get("https://www.rust-lang.org/") .get("https://www.rust-lang.org/")
.append_header(("User-Agent", "Actix-web")); .append_header(("User-Agent", "Actix-web"));
@ -16,7 +16,7 @@ async fn main() -> Result<(), Box<dyn StdError>> {
let mut response = request.send().await?; let mut response = request.send().await?;
// server http response // server response head
println!("Response: {:?}", response); println!("Response: {:?}", response);
// read response body // read response body

View File

@ -207,7 +207,7 @@ where
self self
} }
/// Maximum supported HTTP major version. /// Sets maximum supported HTTP major version.
/// ///
/// Supported versions are HTTP/1.1 and HTTP/2. /// Supported versions are HTTP/1.1 and HTTP/2.
pub fn max_http_version(mut self, val: http::Version) -> Self { pub fn max_http_version(mut self, val: http::Version) -> Self {
@ -222,8 +222,8 @@ where
self self
} }
/// Indicates the initial window size (in octets) for /// Sets the initial window size (in octets) for HTTP/2 stream-level flow control for
/// HTTP2 stream-level flow control for received data. /// received data.
/// ///
/// The default value is 65,535 and is good for APIs, but not for big objects. /// The default value is 65,535 and is good for APIs, but not for big objects.
pub fn initial_window_size(mut self, size: u32) -> Self { pub fn initial_window_size(mut self, size: u32) -> Self {
@ -231,8 +231,8 @@ where
self self
} }
/// Indicates the initial window size (in octets) for /// Sets the initial window size (in octets) for HTTP/2 connection-level flow control for
/// HTTP2 connection-level flow control for received data. /// received data.
/// ///
/// The default value is 65,535 and is good for APIs, but not for big objects. /// The default value is 65,535 and is good for APIs, but not for big objects.
pub fn initial_connection_window_size(mut self, size: u32) -> Self { pub fn initial_connection_window_size(mut self, size: u32) -> Self {
@ -243,6 +243,7 @@ where
/// Set total number of simultaneous connections per type of scheme. /// Set total number of simultaneous connections per type of scheme.
/// ///
/// If limit is 0, the connector has no limit. /// If limit is 0, the connector has no limit.
///
/// The default limit size is 100. /// The default limit size is 100.
pub fn limit(mut self, limit: usize) -> Self { pub fn limit(mut self, limit: usize) -> Self {
self.config.limit = limit; self.config.limit = limit;

Some files were not shown because too many files have changed in this diff Show More