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

Compare commits

..

72 Commits

Author SHA1 Message Date
5a162932f3 prepare awc release 3.0.0-beta.5 2021-04-17 15:30:31 +01:00
b2d6b6a70c prepare web release 4.0.0-beta.6 2021-04-17 15:28:13 +01:00
f743e885a3 prepare http release 3.0.0-beta.6 2021-04-17 15:24:18 +01:00
5747f84736 bump utils to stable v3 2021-04-17 02:07:33 +01:00
879a4cbcd8 re-export ready boilerplate macros in dev 2021-04-16 23:21:02 +01:00
2449f2555c missed one pipeline_factory 2021-04-16 20:48:37 +01:00
d8f56eee3e bump service to stable v2 2021-04-16 20:28:21 +01:00
8d88a0a9af rename header generator macros 2021-04-16 19:15:10 +01:00
845c02cb86 Add responder impl for Cow<str> (#2164) 2021-04-16 00:54:51 +01:00
64bed506c2 chore: update benchmaks to round 20 (#2163) 2021-04-15 19:11:30 +01:00
ff65f1d006 non exhaustive http errors (#2161) 2021-04-14 06:07:59 +01:00
a9f26286f9 reduce branches in h1 dispatcher poll_keepalive (#2089) 2021-04-14 05:20:45 +01:00
037ac80a32 document messagebody trait items 2021-04-14 03:23:15 +01:00
1bfdfd1f41 implement parts as assoc method 2021-04-14 02:57:28 +01:00
5202bf03c1 add some doc examples to response builder 2021-04-14 02:45:58 +01:00
387c229f28 move response builder code to own file 2021-04-14 02:12:47 +01:00
23e0c9b6e0 remove http-codes builders from actix-http (#2159) 2021-04-14 02:00:14 +01:00
02ced426fd add body to_bytes helper (#2158) 2021-04-13 13:34:22 +01:00
4442535a45 clippy 2021-04-13 12:44:38 +01:00
edd9f14752 remove unpin from body types (#2152) 2021-04-13 11:16:12 +01:00
ce50cc9523 files: Don't use canonical path when serving file (#2156) 2021-04-13 05:28:30 +01:00
981c54432c remove json and url encoded form support from -http (#2148) 2021-04-12 10:30:28 +01:00
44c55dd036 remove cookie support from -http (#2065) 2021-04-09 18:07:10 +01:00
c72d77065d derive debug where possible (#2142) 2021-04-09 03:22:51 +01:00
44a2d2214c update year in MIT license (#2143) 2021-04-09 01:28:35 +01:00
3f5a73793a make module/crate re-exports doc inline (#2141) 2021-04-08 20:51:16 +01:00
e0b2246c68 prepare test release 0.1.0-beta.1 2021-04-02 10:03:01 +01:00
e0ae8e59bf prepare actors release 4.0.0-beta.4 2021-04-02 09:55:35 +01:00
a9641e475a prepare http-test release 3.0.0-beta.4 2021-04-02 09:54:35 +01:00
05c7505563 prepare multipart release 0.4.0-beta.4 2021-04-02 09:45:31 +01:00
8561263545 prepare files release 0.6.0-beta.4 2021-04-02 09:43:51 +01:00
a32151525c prepare awc release 3.0.0-beta.4 2021-04-02 09:40:36 +01:00
546e7c5da4 prepare web release 4.0.0-beta.5 2021-04-02 09:37:51 +01:00
6fb06a720a prepare http release 3.0.0-beta.5 2021-04-02 09:27:11 +01:00
c54a0713de migrate integration testing to new crate (#2112) 2021-04-02 08:26:59 +01:00
50dc13f280 move typed headers and implement FromRequest (#2094)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-04-01 16:42:18 +01:00
c8ed8dd1a4 migrate to -utils beta 4 (#2127) 2021-04-01 15:26:13 +01:00
a807d33600 added TestServer::client_headers (#2097)
Co-authored-by: fakeshadow <24548779@qq.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-04-01 06:40:10 +01:00
1f1be6fd3d add Client::headers (#2114) 2021-03-31 11:43:56 +01:00
c49fe79207 Simplify lifetime annotation in HttpServiceBuilder. Simplify PlStream (#2129) 2021-03-30 15:46:09 +01:00
f66774e30b remove From<OffsetDateTime> impl from HttpDate
fully removes time crate from public api of -http
2021-03-30 03:32:22 +01:00
1281a748d0 merge H1ServiceHandler requests into HttpServiceHandler (#2126) 2021-03-30 03:06:16 +01:00
222acfd070 Fix build for next actix-tls-beta release (#2122) 2021-03-29 13:45:48 +01:00
980ecc5f07 fix openssl windows ci 2021-03-29 13:01:37 +01:00
e8ce73b496 update dep docs 2021-03-29 11:52:59 +01:00
f954a30c34 Fix typo in CHANGES.md (#2124) 2021-03-29 10:18:05 +01:00
60f9cfbb2a Refactor actix_http::h2::service module. Reduce loc. (#2118) 2021-03-26 18:24:51 +00:00
6822bf2f58 Refactor actix_http::h1::service (#2117) 2021-03-26 16:15:04 +00:00
2f7f1fa97a fix broken pipe for h2 when client is instantly dropped (#2113) 2021-03-26 00:05:31 +00:00
8c2ce2dedb fix awc compress feature (#2116) 2021-03-25 22:47:37 +00:00
3188ef5731 don't use rust annotation on code doc blocks 2021-03-25 08:45:52 +00:00
9704beddf8 Relax MessageBody limit to 2048kb (#2110)
* relax MessageBody limit to 2048kb

* fix clippy

* Update awc/src/response.rs

Co-authored-by: Rob Ede <robjtede@icloud.com>

* fix default body limit

Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-03-24 04:44:03 -07:00
1be54efbeb Simplify service factory macro (#2108) 2021-03-23 13:42:46 +00:00
746d983849 handle header error with CustomResponder (#2093) 2021-03-20 05:18:06 +00:00
8d9de76826 Simplify handler factory macro (#2086) 2021-03-19 16:30:53 +00:00
9488757c29 Update to socket2 v0.4 (#2092) 2021-03-19 12:17:06 +00:00
351286486c fix clippy warning on nightly (#2088)
* fix clippy warning on nightly
2021-03-19 19:25:35 +08:00
78fcd0237a Format extract macro (#2087) 2021-03-19 04:08:23 +00:00
81942d31d6 fix new dyn trait lint 2021-03-19 02:03:09 +00:00
b75b5114c3 refactor actix_http connection types and connector services (#2081) 2021-03-18 17:53:22 +00:00
abcb444dd9 fix routes in Path documentation (#2084) 2021-03-18 13:21:44 +00:00
983b6904a7 unvendor openssl 2021-03-17 00:38:54 +00:00
3dc2d145ef import some traits as _ 2021-03-17 00:38:54 +00:00
c8f6d37290 rename client io trait. reduce duplicate code (#2079) 2021-03-16 16:31:14 +00:00
69dd1a9bd6 Remove ConnectionLifetime trait. Simplify Acquired handling (#2072) 2021-03-16 02:56:23 +00:00
d93314a683 fix awc readme example (#2076) 2021-03-15 10:59:42 +00:00
a55e87faaa refactor actix_http::helpers to generic over bufmut trait (#2069) 2021-03-15 02:33:51 +00:00
515d0e3fb4 change behavior of default upgrade handler (#2071) 2021-03-13 22:20:18 +00:00
22dcc31193 Fix logger middleware properly escape %% (#2067) 2021-03-11 14:12:42 +00:00
909ef0344b document client mod removal
closes #2064
2021-03-11 00:43:03 +00:00
a2b0e86632 simplify connector generic type (#2063) 2021-03-10 23:57:32 +00:00
d0c1f1a84c remove actix_http::client::pool::Protocol (#2061) 2021-03-10 01:31:50 +00:00
195 changed files with 6431 additions and 5084 deletions

7
.cargo/config.toml Normal file
View File

@ -0,0 +1,7 @@
[alias]
chk = "hack check --workspace --all-features --tests --examples"
lint = "hack --clean-per-run clippy --workspace --tests --examples"
ci-min = "hack check --workspace --no-default-features"
ci-min-test = "hack check --workspace --no-default-features --tests --examples"
ci-default = "hack check --workspace"
ci-full = "check --workspace --bins --examples --tests"

View File

@ -23,9 +23,27 @@ jobs:
name: ${{ matrix.target.name }} / ${{ matrix.version }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
env:
VCPKGRS_DYNAMIC: 1
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
# install OpenSSL on Windows
- name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install OpenSSL
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: vcpkg install openssl:x64-windows
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Install ${{ matrix.version }} - name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@ -50,7 +68,13 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: hack command: hack
args: --clean-per-run check --workspace --no-default-features --tests args: check --workspace --no-default-features
- name: check minimal + tests
uses: actions-rs/cargo@v1
with:
command: hack
args: check --workspace --no-default-features --tests --examples
- name: check full - name: check full
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1

View File

@ -3,6 +3,42 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.6 - 2021-04-17
### Added
* `HttpResponse` and `HttpResponseBuilder` structs. [#2065]
### Changed
* Most error types are now marked `#[non_exhaustive]`. [#2148]
[#2065]: https://github.com/actix/actix-web/pull/2065
[#2148]: https://github.com/actix/actix-web/pull/2148
## 4.0.0-beta.5 - 2021-04-02
### Added
* `Header` extractor for extracting common HTTP headers in handlers. [#2094]
* Added `TestServer::client_headers` method. [#2097]
### Fixed
* Double ampersand in Logger format is escaped correctly. [#2067]
### Changed
* `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed
instead of skipping. (Only the first error is kept when multiple error occur) [#2093]
### Removed
* The `client` mod was removed. Clients should now use `awc` directly.
[871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7)
* Integration testing was moved to new `actix-test` crate. Namely these items from the `test`
module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112]
[#2067]: https://github.com/actix/actix-web/pull/2067
[#2093]: https://github.com/actix/actix-web/pull/2093
[#2094]: https://github.com/actix/actix-web/pull/2094
[#2097]: https://github.com/actix/actix-web/pull/2097
[#2112]: https://github.com/actix/actix-web/pull/2112
## 4.0.0-beta.4 - 2021-03-09 ## 4.0.0-beta.4 - 2021-03-09
### Changed ### Changed
* Feature `cookies` is now optional and enabled by default. [#1981] * Feature `cookies` is now optional and enabled by default. [#1981]

View File

@ -1,26 +1,23 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.4" version = "4.0.0-beta.6"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.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"
readme = "README.md"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
categories = [
"network-programming",
"asynchronous",
"web-programming::http-server",
"web-programming::websocket"
]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web"
documentation = "https://docs.rs/actix-web/"
categories = ["network-programming", "asynchronous",
"web-programming::http-server",
"web-programming::websocket"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with # features that docs.rs will build with
features = ["openssl", "rustls", "compress", "secure-cookies"] features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"]
[badges]
travis-ci = { repository = "actix/actix-web", branch = "master" }
codecov = { repository = "actix/actix-web", branch = "master", service = "github" }
[lib] [lib]
name = "actix_web" name = "actix_web"
@ -28,33 +25,106 @@ path = "src/lib.rs"
[workspace] [workspace]
members = [ members = [
".", ".",
"awc", "awc",
"actix-http", "actix-http",
"actix-files", "actix-files",
"actix-multipart", "actix-multipart",
"actix-web-actors", "actix-web-actors",
"actix-web-codegen", "actix-web-codegen",
"actix-http-test", "actix-http-test",
"actix-test",
] ]
# enable when MSRV is 1.51+
# resolver = "2"
[features] [features]
default = ["compress", "cookies"] default = ["compress", "cookies"]
# content-encoding support # content-encoding support
compress = ["actix-http/compress", "awc/compress"] compress = ["actix-http/compress"]
# support for cookies # support for cookies
cookies = ["actix-http/cookies", "awc/cookies"] cookies = ["cookie"]
# secure cookies feature # secure cookies feature
secure-cookies = ["actix-http/secure-cookies"] secure-cookies = ["cookie/secure"]
# openssl # openssl
openssl = ["tls-openssl", "actix-tls/accept", "actix-tls/openssl", "awc/openssl"] openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
# rustls # rustls
rustls = ["tls-rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"] rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
[dependencies]
actix-codec = "0.4.0-beta.1"
actix-macros = "0.2.0"
actix-router = "0.2.7"
actix-rt = "2.2"
actix-server = "2.0.0-beta.3"
actix-service = "2.0.0"
actix-utils = "3.0.0"
actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true }
actix-web-codegen = "0.5.0-beta.2"
actix-http = "3.0.0-beta.6"
ahash = "0.7"
bytes = "1"
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
itoa = "0.4"
language-tags = "0.2"
once_cell = "1.5"
log = "0.4"
mime = "0.3"
pin-project = "1.0.0"
regex = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
smallvec = "1.6"
socket2 = "0.4.0"
time = { version = "0.2.23", default-features = false, features = ["std"] }
url = "2.1"
[dev-dependencies]
actix-test = { version = "0.1.0-beta.1", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.5", features = ["openssl"] }
brotli2 = "0.3.2"
criterion = "0.3"
env_logger = "0.8"
flate2 = "1.0.13"
rand = "0.8"
rcgen = "0.8"
serde_derive = "1.0"
tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.19.0" }
[profile.release]
lto = true
opt-level = 3
codegen-units = 1
[patch.crates-io]
actix-files = { path = "actix-files" }
actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" }
actix-multipart = { path = "actix-multipart" }
actix-test = { path = "actix-test" }
actix-web = { path = "." }
actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" }
awc = { path = "awc" }
[[test]]
name = "test_server"
required-features = ["compress", "cookies"]
[[example]] [[example]]
name = "basic" name = "basic"
@ -64,83 +134,10 @@ required-features = ["compress"]
name = "uds" name = "uds"
required-features = ["compress"] required-features = ["compress"]
[[test]]
name = "test_server"
required-features = ["compress", "cookies"]
[[example]] [[example]]
name = "on_connect" name = "on_connect"
required-features = [] required-features = []
[[example]]
name = "client"
required-features = ["rustls"]
[dependencies]
actix-codec = "0.4.0-beta.1"
actix-macros = "0.2.0"
actix-router = "0.2.7"
actix-rt = "2.1"
actix-server = "2.0.0-beta.3"
actix-service = "2.0.0-beta.4"
actix-utils = "3.0.0-beta.2"
actix-tls = { version = "3.0.0-beta.4", default-features = false, optional = true }
actix-web-codegen = "0.5.0-beta.2"
actix-http = "3.0.0-beta.4"
awc = { version = "3.0.0-beta.3", default-features = false }
ahash = "0.7"
bytes = "1"
derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
log = "0.4"
mime = "0.3"
pin-project = "1.0.0"
regex = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
smallvec = "1.6"
socket2 = "0.3.16"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.19.0", optional = true }
url = "2.1"
[target.'cfg(windows)'.dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
optional = true
[dev-dependencies]
brotli2 = "0.3.2"
criterion = "0.3"
env_logger = "0.8"
flate2 = "1.0.13"
rand = "0.8"
rcgen = "0.8"
serde_derive = "1.0"
[profile.release]
lto = true
opt-level = 3
codegen-units = 1
[patch.crates-io]
actix-web = { path = "." }
actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" }
actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" }
actix-multipart = { path = "actix-multipart" }
actix-files = { path = "actix-files" }
awc = { path = "awc" }
[[bench]] [[bench]]
name = "server" name = "server"
harness = false harness = false

View File

@ -1,4 +1,4 @@
Copyright (c) 2017 Actix Team Copyright (c) 2017-NOW Actix Team
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

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.4)](https://docs.rs/actix-web/4.0.0-beta.4) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.5)](https://docs.rs/actix-web/4.0.0-beta.5)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![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.4/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.4) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.5)
<br /> <br />
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions)
[![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)
@ -90,7 +90,7 @@ You may consider checking out
## 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-r19). [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
## License ## License

View File

@ -1,6 +1,13 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
[#2156]: https://github.com/actix/actix-web/pull/2156
## 0.6.0-beta.4 - 2021-04-02
* No notable changes.
## 0.6.0-beta.3 - 2021-03-09 ## 0.6.0-beta.3 - 2021-03-09

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.0-beta.3" version = "0.6.0-beta.4"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static file serving for Actix Web" description = "Static file serving for Actix Web"
readme = "README.md" readme = "README.md"
@ -17,14 +17,14 @@ name = "actix_files"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.4", default-features = false } actix-web = { version = "4.0.0-beta.6", default-features = false }
actix-service = "2.0.0-beta.4" actix-service = "2.0.0"
actix-utils = "3.0.0"
askama_escape = "0.10" askama_escape = "0.10"
bitflags = "1" bitflags = "1"
bytes = "1" bytes = "1"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false }
http-range = "0.1.4" http-range = "0.1.4"
derive_more = "0.99.5" derive_more = "0.99.5"
log = "0.4" log = "0.4"
@ -33,5 +33,6 @@ mime_guess = "2.0.1"
percent-encoding = "2.1" percent-encoding = "2.1"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.1" actix-rt = "2.2"
actix-web = "4.0.0-beta.4" actix-web = "4.0.0-beta.6"
actix-test = "0.1.0-beta.1"

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.3)](https://docs.rs/actix-files/0.6.0-beta.3) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.4)](https://docs.rs/actix-files/0.6.0-beta.4)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.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.3/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.3) [![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.4/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.4)
[![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)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -1,4 +1,4 @@
use actix_web::{http::StatusCode, HttpResponse, ResponseError}; use actix_web::{http::StatusCode, ResponseError};
use derive_more::Display; use derive_more::Display;
/// Errors which can occur when serving static files. /// Errors which can occur when serving static files.
@ -16,8 +16,8 @@ pub enum FilesError {
/// Return `NotFound` for `FilesError` /// Return `NotFound` for `FilesError`
impl ResponseError for FilesError { impl ResponseError for FilesError {
fn error_response(&self) -> HttpResponse { fn status_code(&self) -> StatusCode {
HttpResponse::new(StatusCode::NOT_FOUND) StatusCode::NOT_FOUND
} }
} }

View File

@ -1,6 +1,7 @@
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
error::Error, error::Error,
@ -8,7 +9,7 @@ use actix_web::{
http::header::DispositionType, http::header::DispositionType,
HttpRequest, HttpRequest,
}; };
use futures_util::future::{ok, FutureExt, LocalBoxFuture}; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
@ -19,7 +20,7 @@ use crate::{
/// ///
/// `Files` service must be registered with `App::service()` method. /// `Files` service must be registered with `App::service()` method.
/// ///
/// ```rust /// ```
/// use actix_web::App; /// use actix_web::App;
/// use actix_files::Files; /// use actix_files::Files;
/// ///
@ -263,18 +264,18 @@ impl ServiceFactory<ServiceRequest> for Files {
}; };
if let Some(ref default) = *self.default.borrow() { if let Some(ref default) = *self.default.borrow() {
default let fut = default.new_service(());
.new_service(()) Box::pin(async {
.map(move |result| match result { match fut.await {
Ok(default) => { Ok(default) => {
srv.default = Some(default); srv.default = Some(default);
Ok(srv) Ok(srv)
} }
Err(_) => Err(()), Err(_) => Err(()),
}) }
.boxed_local() })
} else { } else {
ok(srv).boxed_local() Box::pin(ok(srv))
} }
} }
} }

View File

@ -3,7 +3,7 @@
//! Provides a non-blocking service for serving static files from disk. //! Provides a non-blocking service for serving static files from disk.
//! //!
//! # Example //! # Example
//! ```rust //! ```
//! use actix_web::App; //! use actix_web::App;
//! use actix_files::Files; //! use actix_files::Files;
//! //!
@ -65,6 +65,7 @@ mod tests {
}; };
use actix_service::ServiceFactory; use actix_service::ServiceFactory;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
guard, guard,
http::{ http::{
@ -76,7 +77,6 @@ mod tests {
web::{self, Bytes}, web::{self, Bytes},
App, HttpResponse, Responder, App, HttpResponse, Responder,
}; };
use futures_util::future::ok;
use super::*; use super::*;
@ -413,7 +413,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_range_headers() { async fn test_named_file_content_range_headers() {
let srv = test::start(|| App::new().service(Files::new("/", "."))); let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header // Valid range header
let response = srv let response = srv
@ -438,7 +438,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_length_headers() { async fn test_named_file_content_length_headers() {
let srv = test::start(|| App::new().service(Files::new("/", "."))); let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header // Valid range header
let response = srv let response = srv
@ -477,7 +477,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_head_content_length_headers() { async fn test_head_content_length_headers() {
let srv = test::start(|| App::new().service(Files::new("/", "."))); let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
let response = srv.head("/tests/test.binary").send().await.unwrap(); let response = srv.head("/tests/test.binary").send().await.unwrap();
@ -754,4 +754,19 @@ mod tests {
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
} }
#[actix_rt::test]
async fn test_symlinks() {
let srv = test::init_service(App::new().service(Files::new("test", "."))).await;
let req = TestRequest::get()
.uri("/test/tests/symlink-test.png")
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"symlink-test.png\""
);
}
} }

View File

@ -60,7 +60,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// use std::io::{self, Write}; /// use std::io::{self, Write};
/// use std::env; /// use std::env;
@ -137,7 +137,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
/// let file = NamedFile::open("foo.txt"); /// let file = NamedFile::open("foo.txt");
@ -156,7 +156,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// # use std::io; /// # use std::io;
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///

View File

@ -3,8 +3,8 @@ use std::{
str::FromStr, str::FromStr,
}; };
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, FromRequest, HttpRequest}; use actix_web::{dev::Payload, FromRequest, HttpRequest};
use futures_util::future::{ready, Ready};
use crate::error::UriSegmentError; use crate::error::UriSegmentError;

View File

@ -1,6 +1,7 @@
use std::{fmt, io, path::PathBuf, rc::Rc, task::Poll}; use std::{fmt, io, path::PathBuf, rc::Rc};
use actix_service::Service; use actix_service::Service;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
error::Error, error::Error,
@ -8,7 +9,7 @@ use actix_web::{
http::{header, Method}, http::{header, Method},
HttpResponse, HttpResponse,
}; };
use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
@ -29,19 +30,18 @@ pub struct FilesService {
pub(crate) hidden_files: bool, pub(crate) hidden_files: bool,
} }
type FilesServiceFuture = Either<
Ready<Result<ServiceResponse, Error>>,
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
>;
impl FilesService { impl FilesService {
fn handle_err(&self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture { fn handle_err(
log::debug!("Failed to handle {}: {}", req.path(), e); &self,
err: io::Error,
req: ServiceRequest,
) -> LocalBoxFuture<'static, Result<ServiceResponse, Error>> {
log::debug!("error handling {}: {}", req.path(), err);
if let Some(ref default) = self.default { if let Some(ref default) = self.default {
Either::Right(default.call(req)) Box::pin(default.call(req))
} else { } else {
Either::Left(ok(req.error_response(e))) Box::pin(ok(req.error_response(err)))
} }
} }
} }
@ -55,7 +55,7 @@ impl fmt::Debug for FilesService {
impl Service<ServiceRequest> for FilesService { impl Service<ServiceRequest> for FilesService {
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Future = FilesServiceFuture; type Future = LocalBoxFuture<'static, Result<ServiceResponse, Error>>;
actix_service::always_ready!(); actix_service::always_ready!();
@ -69,7 +69,7 @@ impl Service<ServiceRequest> for FilesService {
}; };
if !is_method_valid { if !is_method_valid {
return Either::Left(ok(req.into_response( return Box::pin(ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed() actix_web::HttpResponse::MethodNotAllowed()
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
.body("Request did not meet this resource's requirements."), .body("Request did not meet this resource's requirements."),
@ -79,21 +79,21 @@ impl Service<ServiceRequest> for FilesService {
let real_path = let real_path =
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) { match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
Ok(item) => item, Ok(item) => item,
Err(e) => return Either::Left(ok(req.error_response(e))), Err(e) => return Box::pin(ok(req.error_response(e))),
}; };
// full file path // full file path
let path = match self.directory.join(&real_path).canonicalize() { let path = self.directory.join(&real_path);
Ok(path) => path, if let Err(err) = path.canonicalize() {
Err(e) => return self.handle_err(e, req), return Box::pin(self.handle_err(err, req));
}; }
if path.is_dir() { if path.is_dir() {
if let Some(ref redir_index) = self.index { if let Some(ref redir_index) = self.index {
if self.redirect_to_slash && !req.path().ends_with('/') { if self.redirect_to_slash && !req.path().ends_with('/') {
let redirect_to = format!("{}/", req.path()); let redirect_to = format!("{}/", req.path());
return Either::Left(ok(req.into_response( return Box::pin(ok(req.into_response(
HttpResponse::Found() HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to)) .insert_header((header::LOCATION, redirect_to))
.body("") .body("")
@ -114,9 +114,9 @@ impl Service<ServiceRequest> for FilesService {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); let res = named_file.into_response(&req);
Either::Left(ok(ServiceResponse::new(req, res))) Box::pin(ok(ServiceResponse::new(req, res)))
} }
Err(e) => self.handle_err(e, req), Err(err) => self.handle_err(err, req),
} }
} else if self.show_index { } else if self.show_index {
let dir = Directory::new(self.directory.clone(), path); let dir = Directory::new(self.directory.clone(), path);
@ -124,12 +124,12 @@ impl Service<ServiceRequest> for FilesService {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let x = (self.renderer)(&dir, &req); let x = (self.renderer)(&dir, &req);
match x { Box::pin(match x {
Ok(resp) => Either::Left(ok(resp)), Ok(resp) => ok(resp),
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), Err(err) => ok(ServiceResponse::from_err(err, req)),
} })
} else { } else {
Either::Left(ok(ServiceResponse::from_err( Box::pin(ok(ServiceResponse::from_err(
FilesError::IsDirectory, FilesError::IsDirectory,
req.into_parts().0, req.into_parts().0,
))) )))
@ -145,9 +145,9 @@ impl Service<ServiceRequest> for FilesService {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); let res = named_file.into_response(&req);
Either::Left(ok(ServiceResponse::new(req, res))) Box::pin(ok(ServiceResponse::new(req, res)))
} }
Err(e) => self.handle_err(e, req), Err(err) => self.handle_err(err, req),
} }
} }
} }

View File

@ -0,0 +1 @@
test.png

View File

@ -3,6 +3,12 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.4 - 2021-04-02
* Added `TestServer::client_headers` method. [#2097]
[#2097]: https://github.com/actix/actix-web/pull/2097
## 3.0.0-beta.3 - 2021-03-09 ## 3.0.0-beta.3 - 2021-03-09
* No notable changes. * No notable changes.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.3" version = "3.0.0-beta.4"
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"
readme = "README.md" readme = "README.md"
@ -29,20 +29,20 @@ default = []
openssl = ["tls-openssl", "awc/openssl"] openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2.0.0-beta.4" actix-service = "2.0.0"
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0-beta.1"
actix-tls = "3.0.0-beta.4" actix-tls = "3.0.0-beta.5"
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0"
actix-rt = "2.1" actix-rt = "2.2"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
awc = { version = "3.0.0-beta.3", default-features = false } awc = { version = "3.0.0-beta.5", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
http = "0.2.2" http = "0.2.2"
log = "0.4" log = "0.4"
socket2 = "0.3" socket2 = "0.4"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
slab = "0.4" slab = "0.4"
@ -50,12 +50,6 @@ serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] } time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
[target.'cfg(windows)'.dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
optional = true
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.4", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.6", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.4" actix-http = "3.0.0-beta.6"

View File

@ -3,9 +3,9 @@
> 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.3)](https://docs.rs/actix-http-test/3.0.0-beta.3) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4)
![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)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.3/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.3) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources ## Documentation & Resources

View File

@ -13,7 +13,9 @@ use std::{net, thread, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::{net::TcpStream, System}; use actix_rt::{net::TcpStream, System};
use actix_server::{Server, ServiceFactory}; use actix_server::{Server, ServiceFactory};
use awc::{error::PayloadError, ws, Client, ClientRequest, ClientResponse, Connector}; use awc::{
error::PayloadError, http::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector,
};
use bytes::Bytes; use bytes::Bytes;
use futures_core::stream::Stream; use futures_core::stream::Stream;
use http::Method; use http::Method;
@ -26,7 +28,7 @@ use socket2::{Domain, Protocol, Socket, Type};
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use actix_http::HttpService; /// use actix_http::HttpService;
/// use actix_http_test::TestServer; /// use actix_http_test::TestServer;
/// use actix_web::{web, App, HttpResponse, Error}; /// use actix_web::{web, App, HttpResponse, Error};
@ -115,16 +117,6 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
} }
} }
/// Get first available unused address
pub fn unused_addr() -> net::SocketAddr {
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
socket.bind(&addr.into()).unwrap();
socket.set_reuse_address(true).unwrap();
let tcp = socket.into_tcp_listener();
tcp.local_addr().unwrap()
}
/// Test server controller /// Test server controller
pub struct TestServer { pub struct TestServer {
addr: net::SocketAddr, addr: net::SocketAddr,
@ -258,6 +250,14 @@ impl TestServer {
self.ws_at("/").await self.ws_at("/").await
} }
/// Get default HeaderMap of Client.
///
/// Returns Some(&mut HeaderMap) when Client object is unique
/// (No other clone of client exists at the same time).
pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
self.client.headers()
}
/// Stop HTTP server /// Stop HTTP server
fn stop(&mut self) { fn stop(&mut self) {
self.system.stop(); self.system.stop();
@ -269,3 +269,13 @@ impl Drop for TestServer {
self.stop() self.stop()
} }
} }
/// Get a localhost socket address with random, unused port.
pub fn unused_addr() -> net::SocketAddr {
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
socket.bind(&addr.into()).unwrap();
socket.set_reuse_address(true).unwrap();
let tcp = net::TcpListener::from(socket);
tcp.local_addr().unwrap()
}

View File

@ -3,6 +3,57 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.6 - 2021-04-17
### Added
* `impl<T: MessageBody> MessageBody for Pin<Box<T>>`. [#2152]
* `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
* Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
### Changes
* The type parameter of `Response` no longer has a default. [#2152]
* The `Message` variant of `body::Body` is now `Pin<Box<dyn MessageBody>>`. [#2152]
* `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152]
* Error enum types are marked `#[non_exhaustive]`. [#2161]
### Removed
* `cookies` feature flag. [#2065]
* Top-level `cookies` mod (re-export). [#2065]
* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065]
* `impl ResponseError for CookieParseError`. [#2065]
* Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148]
* `ResponseBuilder::json`. [#2148]
* `ResponseBuilder::{set_header, header}`. [#2148]
* `impl From<serde_json::Value> for Body`. [#2148]
* `Response::build_from`. [#2159]
* Most of the status code builders on `Response`. [#2159]
[#2065]: https://github.com/actix/actix-web/pull/2065
[#2148]: https://github.com/actix/actix-web/pull/2148
[#2152]: https://github.com/actix/actix-web/pull/2152
[#2159]: https://github.com/actix/actix-web/pull/2159
[#2158]: https://github.com/actix/actix-web/pull/2158
[#2161]: https://github.com/actix/actix-web/pull/2161
## 3.0.0-beta.5 - 2021-04-02
### Added
* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
* `client::ConnectionIo` trait alias [#2081]
### Changed
* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063]
### Removed
* Common typed HTTP headers were moved to actix-web. [2094]
* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
[#2063]: https://github.com/actix/actix-web/pull/2063
[#2081]: https://github.com/actix/actix-web/pull/2081
[#2094]: https://github.com/actix/actix-web/pull/2094
[#2127]: https://github.com/actix/actix-web/pull/2127
## 3.0.0-beta.4 - 2021-03-08 ## 3.0.0-beta.4 - 2021-03-08
### Changed ### Changed
* Feature `cookies` is now optional and disabled by default. [#1981] * Feature `cookies` is now optional and disabled by default. [#1981]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.4" version = "3.0.0-beta.6"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
readme = "README.md" readme = "README.md"
@ -16,7 +16,7 @@ edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with # features that docs.rs will build with
features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"] features = ["openssl", "rustls", "compress"]
[lib] [lib]
name = "actix_http" name = "actix_http"
@ -34,29 +34,21 @@ rustls = ["actix-tls/rustls"]
# enable compression support # enable compression support
compress = ["flate2", "brotli2"] compress = ["flate2", "brotli2"]
# support for cookies
cookies = ["cookie"]
# support for secure cookies
secure-cookies = ["cookies", "cookie/secure"]
# trust-dns as client dns resolver # trust-dns as client dns resolver
trust-dns = ["trust-dns-resolver"] trust-dns = ["trust-dns-resolver"]
[dependencies] [dependencies]
actix-service = "2.0.0-beta.4" actix-service = "2.0.0"
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0-beta.1"
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0"
actix-rt = "2.1" actix-rt = "2.2"
actix-tls = "3.0.0-beta.4" actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] }
ahash = "0.7" ahash = "0.7"
base64 = "0.13" base64 = "0.13"
bitflags = "1.2" bitflags = "1.2"
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
cfg-if = "1"
cookie = { version = "0.14.1", features = ["percent-encode"], optional = true }
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"] }
@ -66,16 +58,17 @@ http = "0.2.2"
httparse = "1.3" httparse = "1.3"
itoa = "0.4" itoa = "0.4"
language-tags = "0.2" language-tags = "0.2"
local-channel = "0.1"
once_cell = "1.5" once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
paste = "1"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project = "1.0.0" pin-project = "1.0.0"
pin-project-lite = "0.2"
rand = "0.8" rand = "0.8"
regex = "1.3" regex = "1.3"
serde = "1.0" serde = "1.0"
serde_json = "1.0"
serde_urlencoded = "0.7"
sha-1 = "0.9" sha-1 = "0.9"
smallvec = "1.6" smallvec = "1.6"
time = { version = "0.2.23", default-features = false, features = ["std"] } time = { version = "0.2.23", default-features = false, features = ["std"] }
@ -89,20 +82,16 @@ trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
actix-http-test = { version = "3.0.0-beta.3", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.4", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] }
criterion = "0.3" criterion = "0.3"
env_logger = "0.8" env_logger = "0.8"
rcgen = "0.8" rcgen = "0.8"
serde_derive = "1.0" serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tls-openssl = { version = "0.10", package = "openssl" } tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" } tls-rustls = { version = "0.19", package = "rustls" }
[target.'cfg(windows)'.dev-dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
[[example]] [[example]]
name = "ws" name = "ws"
required-features = ["rustls"] required-features = ["rustls"]

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.4)](https://docs.rs/actix-http/3.0.0-beta.4) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.6)](https://docs.rs/actix-http/3.0.0-beta.6)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.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.4/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.4) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.6)
[![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)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -1,9 +1,9 @@
use std::{env, io}; use std::{env, io};
use actix_http::{Error, HttpService, Request, Response}; use actix_http::{http::StatusCode, Error, HttpService, Request, Response};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt; use futures_util::StreamExt as _;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info; use log::info;
@ -25,7 +25,7 @@ async fn main() -> io::Result<()> {
info!("request body: {:?}", body); info!("request body: {:?}", body);
Ok::<_, Error>( Ok::<_, Error>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header(( .insert_header((
"x-head", "x-head",
HeaderValue::from_static("dummy value!"), HeaderValue::from_static("dummy value!"),

View File

@ -1,20 +1,20 @@
use std::{env, io}; use std::{env, io};
use actix_http::http::HeaderValue; use actix_http::{body::Body, http::HeaderValue, http::StatusCode};
use actix_http::{Error, HttpService, Request, Response}; use actix_http::{Error, HttpService, Request, Response};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt; use futures_util::StreamExt as _;
use log::info; use log::info;
async fn handle_request(mut req: Request) -> Result<Response, Error> { async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> {
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 {
body.extend_from_slice(&item?) body.extend_from_slice(&item?)
} }
info!("request body: {:?}", body); info!("request body: {:?}", body);
Ok(Response::Ok() 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))
} }

View File

@ -1,8 +1,8 @@
use std::{env, io}; use std::{env, io};
use actix_http::{HttpService, Response}; use actix_http::{http::StatusCode, HttpService, Response};
use actix_server::Server; use actix_server::Server;
use futures_util::future; use actix_utils::future;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info; use log::info;
@ -18,7 +18,7 @@ async fn main() -> io::Result<()> {
.client_disconnect(1000) .client_disconnect(1000)
.finish(|_req| { .finish(|_req| {
info!("{:?}", _req); info!("{:?}", _req);
let mut res = Response::Ok(); let mut res = Response::build(StatusCode::OK);
res.insert_header(( res.insert_header((
"x-head", "x-head",
HeaderValue::from_static("dummy value!"), HeaderValue::from_static("dummy value!"),

View File

@ -11,7 +11,7 @@ use std::{
}; };
use actix_codec::Encoder; use actix_codec::Encoder;
use actix_http::{error::Error, ws, HttpService, Request, Response}; use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
use actix_rt::time::{interval, Interval}; use actix_rt::time::{interval, Interval};
use actix_server::Server; use actix_server::Server;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
@ -34,14 +34,14 @@ async fn main() -> io::Result<()> {
.await .await
} }
async fn handler(req: Request) -> Result<Response, Error> { async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
log::info!("handshaking"); log::info!("handshaking");
let mut res = ws::handshake(req.head())?; let mut res = ws::handshake(req.head())?;
// handshake will always fail under HTTP/2 // handshake will always fail under HTTP/2
log::info!("responding"); log::info!("responding");
Ok(res.streaming(Heartbeat::new(ws::Codec::new()))) Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new()))))
} }
struct Heartbeat { struct Heartbeat {

View File

@ -1,4 +1,5 @@
use std::{ use std::{
borrow::Cow,
fmt, mem, fmt, mem,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
@ -12,15 +13,19 @@ use crate::error::Error;
use super::{BodySize, BodyStream, MessageBody, SizedStream}; use super::{BodySize, BodyStream, MessageBody, SizedStream};
/// Represents various types of HTTP message body. /// Represents various types of HTTP message body.
// #[deprecated(since = "4.0.0", note = "Use body types directly.")]
pub enum Body { pub enum Body {
/// Empty response. `Content-Length` header is not set. /// Empty response. `Content-Length` header is not set.
None, None,
/// Zero sized response body. `Content-Length` header is set to `0`. /// Zero sized response body. `Content-Length` header is set to `0`.
Empty, Empty,
/// Specific response body. /// Specific response body.
Bytes(Bytes), Bytes(Bytes),
/// Generic message body. /// Generic message body.
Message(Box<dyn MessageBody + Unpin>), Message(Pin<Box<dyn MessageBody>>),
} }
impl Body { impl Body {
@ -30,8 +35,8 @@ impl Body {
} }
/// Create body from generic message body. /// Create body from generic message body.
pub fn from_message<B: MessageBody + Unpin + 'static>(body: B) -> Body { pub fn from_message<B: MessageBody + 'static>(body: B) -> Body {
Body::Message(Box::new(body)) Body::Message(Box::pin(body))
} }
} }
@ -60,7 +65,7 @@ impl MessageBody for Body {
Poll::Ready(Some(Ok(mem::take(bin)))) Poll::Ready(Some(Ok(mem::take(bin))))
} }
} }
Body::Message(body) => Pin::new(&mut **body).poll_next(cx), Body::Message(body) => body.as_mut().poll_next(cx),
} }
} }
} }
@ -114,12 +119,23 @@ impl From<String> for Body {
} }
} }
impl<'a> From<&'a String> for Body { impl From<&'_ String> for Body {
fn from(s: &'a String) -> Body { fn from(s: &String) -> Body {
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s))) Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
} }
} }
impl From<Cow<'_, str>> for Body {
fn from(s: Cow<'_, str>) -> Body {
match s {
Cow::Owned(s) => Body::from(s),
Cow::Borrowed(s) => {
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)))
}
}
}
}
impl From<Bytes> for Body { impl From<Bytes> for Body {
fn from(s: Bytes) -> Body { fn from(s: Bytes) -> Body {
Body::Bytes(s) Body::Bytes(s)
@ -132,15 +148,9 @@ impl From<BytesMut> for Body {
} }
} }
impl From<serde_json::Value> for Body {
fn from(v: serde_json::Value) -> Body {
Body::Bytes(v.to_string().into())
}
}
impl<S> From<SizedStream<S>> for Body impl<S> From<SizedStream<S>> for Body
where where
S: Stream<Item = Result<Bytes, Error>> + Unpin + 'static, S: Stream<Item = Result<Bytes, Error>> + 'static,
{ {
fn from(s: SizedStream<S>) -> Body { fn from(s: SizedStream<S>) -> Body {
Body::from_message(s) Body::from_message(s)
@ -149,7 +159,7 @@ where
impl<S, E> From<BodyStream<S>> for Body impl<S, E> From<BodyStream<S>> for Body
where where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static, S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Error> + 'static, E: Into<Error> + 'static,
{ {
fn from(s: BodyStream<S>) -> Body { fn from(s: BodyStream<S>) -> Body {

View File

@ -5,21 +5,25 @@ use std::{
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
use crate::error::Error; use crate::error::Error;
use super::{BodySize, MessageBody}; use super::{BodySize, MessageBody};
/// Streaming response wrapper. pin_project! {
/// /// Streaming response wrapper.
/// Response does not contain `Content-Length` header and appropriate transfer encoding is used. ///
pub struct BodyStream<S: Unpin> { /// Response does not contain `Content-Length` header and appropriate transfer encoding is used.
stream: S, pub struct BodyStream<S> {
#[pin]
stream: S,
}
} }
impl<S, E> BodyStream<S> impl<S, E> BodyStream<S>
where where
S: Stream<Item = Result<Bytes, E>> + Unpin, S: Stream<Item = Result<Bytes, E>>,
E: Into<Error>, E: Into<Error>,
{ {
pub fn new(stream: S) -> Self { pub fn new(stream: S) -> Self {
@ -29,7 +33,7 @@ where
impl<S, E> MessageBody for BodyStream<S> impl<S, E> MessageBody for BodyStream<S>
where where
S: Stream<Item = Result<Bytes, E>> + Unpin, S: Stream<Item = Result<Bytes, E>>,
E: Into<Error>, E: Into<Error>,
{ {
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
@ -46,9 +50,9 @@ where
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Error>>> {
loop { loop {
let stream = &mut self.as_mut().stream; let stream = self.as_mut().project().stream;
let chunk = match ready!(Pin::new(stream).poll_next(cx)) { let chunk = match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue, Some(Ok(ref bytes)) if bytes.is_empty() => continue,
opt => opt.map(|res| res.map_err(Into::into)), opt => opt.map(|res| res.map_err(Into::into)),
}; };
@ -57,3 +61,49 @@ where
} }
} }
} }
#[cfg(test)]
mod tests {
use actix_rt::pin;
use actix_utils::future::poll_fn;
use futures_util::stream;
use super::*;
use crate::body::to_bytes;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
));
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
#[actix_rt::test]
async fn read_to_bytes() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
));
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
}
}

View File

@ -12,10 +12,12 @@ use crate::error::Error;
use super::BodySize; use super::BodySize;
/// Type that implement this trait can be streamed to a peer. /// An interface for response bodies.
pub trait MessageBody { pub trait MessageBody {
/// Body size hint.
fn size(&self) -> BodySize; fn size(&self) -> BodySize;
/// Attempt to pull out the next chunk of body bytes.
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
@ -52,6 +54,19 @@ impl<T: MessageBody + Unpin> MessageBody for Box<T> {
} }
} }
impl<T: MessageBody> MessageBody for Pin<Box<T>> {
fn size(&self) -> BodySize {
self.as_ref().size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
self.as_mut().poll_next(cx)
}
}
impl MessageBody for Bytes { impl MessageBody for Bytes {
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)

View File

@ -1,5 +1,12 @@
//! Traits and structures to aid consuming and writing HTTP payloads. //! Traits and structures to aid consuming and writing HTTP payloads.
use std::task::Poll;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use futures_core::ready;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod body; mod body;
mod body_stream; mod body_stream;
@ -15,13 +22,57 @@ pub use self::response_body::ResponseBody;
pub use self::size::BodySize; pub use self::size::BodySize;
pub use self::sized_stream::SizedStream; pub use self::sized_stream::SizedStream;
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
///
/// Any errors produced by the body stream are returned immediately.
///
/// # Examples
/// ```
/// use actix_http::body::{Body, to_bytes};
/// use bytes::Bytes;
///
/// # async fn test_to_bytes() {
/// let body = Body::Empty;
/// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty());
///
/// let body = Body::Bytes(Bytes::from_static(b"123"));
/// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, b"123"[..]);
/// # }
/// ```
pub async fn to_bytes(body: impl MessageBody) -> Result<Bytes, crate::Error> {
let cap = match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize,
BodySize::Stream => 32_768,
};
let mut buf = BytesMut::with_capacity(cap);
pin!(body);
poll_fn(|cx| loop {
let body = body.as_mut();
match ready!(body.poll_next(cx)) {
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await?;
Ok(buf.freeze())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::pin::Pin; use std::pin::Pin;
use actix_rt::pin; use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_util::{future::poll_fn, stream};
use super::*; use super::*;
@ -173,69 +224,19 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_serde_json() { async fn test_serde_json() {
use serde_json::json; use serde_json::{json, Value};
assert_eq!( assert_eq!(
Body::from(serde_json::Value::String("test".into())).size(), Body::from(serde_json::to_vec(&Value::String("test".to_owned())).unwrap())
.size(),
BodySize::Sized(6) BodySize::Sized(6)
); );
assert_eq!( assert_eq!(
Body::from(json!({"test-key":"test-value"})).size(), Body::from(serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap())
.size(),
BodySize::Sized(25) BodySize::Sized(25)
); );
} }
#[actix_rt::test]
async fn body_stream_skips_empty_chunks() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
));
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
mod sized_stream {
use super::*;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = SizedStream::new(
2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
);
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
}
#[actix_rt::test] #[actix_rt::test]
async fn test_body_casting() { async fn test_body_casting() {
let mut body = String::from("hello cast"); let mut body = String::from("hello cast");
@ -249,4 +250,15 @@ mod tests {
let not_body = resp_body.downcast_ref::<()>(); let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none()); assert!(not_body.is_none());
} }
#[actix_rt::test]
async fn test_to_bytes() {
let body = Body::Empty;
let bytes = to_bytes(body).await.unwrap();
assert!(bytes.is_empty());
let body = Body::Bytes(Bytes::from_static(b"123"));
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123"[..]);
}
} }

View File

@ -55,10 +55,7 @@ impl<B: MessageBody> MessageBody for ResponseBody<B> {
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Error>>> {
match self.project() { Stream::poll_next(self, cx)
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
}
} }
} }

View File

@ -5,23 +5,27 @@ use std::{
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
use crate::error::Error; use crate::error::Error;
use super::{BodySize, MessageBody}; use super::{BodySize, MessageBody};
/// Known sized streaming response wrapper. pin_project! {
/// /// Known sized streaming response wrapper.
/// This body implementation should be used if total size of stream is known. Data get sent as is ///
/// without using transfer encoding. /// This body implementation should be used if total size of stream is known. Data get sent as is
pub struct SizedStream<S: Unpin> { /// without using transfer encoding.
size: u64, pub struct SizedStream<S> {
stream: S, size: u64,
#[pin]
stream: S,
}
} }
impl<S> SizedStream<S> impl<S> SizedStream<S>
where where
S: Stream<Item = Result<Bytes, Error>> + Unpin, S: Stream<Item = Result<Bytes, Error>>,
{ {
pub fn new(size: u64, stream: S) -> Self { pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream } SizedStream { size, stream }
@ -30,7 +34,7 @@ where
impl<S> MessageBody for SizedStream<S> impl<S> MessageBody for SizedStream<S>
where where
S: Stream<Item = Result<Bytes, Error>> + Unpin, S: Stream<Item = Result<Bytes, Error>>,
{ {
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64) BodySize::Sized(self.size as u64)
@ -46,9 +50,9 @@ where
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Error>>> {
loop { loop {
let stream = &mut self.as_mut().stream; let stream = self.as_mut().project().stream;
let chunk = match ready!(Pin::new(stream).poll_next(cx)) { let chunk = match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue, Some(Ok(ref bytes)) if bytes.is_empty() => continue,
val => val, val => val,
}; };
@ -57,3 +61,49 @@ where
} }
} }
} }
#[cfg(test)]
mod tests {
use actix_rt::pin;
use actix_utils::future::poll_fn;
use futures_util::stream;
use super::*;
use crate::body::to_bytes;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = SizedStream::new(
2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
);
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
#[actix_rt::test]
async fn read_to_bytes() {
let body = SizedStream::new(
2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
);
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
}
}

View File

@ -63,11 +63,9 @@ where
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
{ {
/// Set server keep-alive setting. /// Set server keep-alive setting.
/// ///
@ -127,7 +125,6 @@ where
X1: ServiceFactory<Request, Config = (), Response = Request>, X1: ServiceFactory<Request, Config = (), Response = Request>,
X1::Error: Into<Error>, X1::Error: Into<Error>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
<X1::Service as Service<Request>>::Future: 'static,
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
@ -152,7 +149,6 @@ where
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U1::Error: fmt::Display, U1::Error: fmt::Display,
U1::InitError: fmt::Debug, U1::InitError: fmt::Debug,
<U1::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
@ -211,7 +207,6 @@ where
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
@ -233,7 +228,6 @@ where
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,

View File

@ -8,6 +8,7 @@ const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ConnectorConfig { pub(crate) struct ConnectorConfig {
pub(crate) timeout: Duration, pub(crate) timeout: Duration,
pub(crate) handshake_timeout: Duration,
pub(crate) conn_lifetime: Duration, pub(crate) conn_lifetime: Duration,
pub(crate) conn_keep_alive: Duration, pub(crate) conn_keep_alive: Duration,
pub(crate) disconnect_timeout: Option<Duration>, pub(crate) disconnect_timeout: Option<Duration>,
@ -21,6 +22,7 @@ impl Default for ConnectorConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
timeout: Duration::from_secs(5), timeout: Duration::from_secs(5),
handshake_timeout: Duration::from_secs(5),
conn_lifetime: Duration::from_secs(75), conn_lifetime: Duration::from_secs(75),
conn_keep_alive: Duration::from_secs(15), conn_keep_alive: Duration::from_secs(15),
disconnect_timeout: Some(Duration::from_millis(3000)), disconnect_timeout: Some(Duration::from_millis(3000)),

View File

@ -1,14 +1,16 @@
use std::ops::{Deref, DerefMut}; use std::{
use std::pin::Pin; io,
use std::task::{Context, Poll}; ops::{Deref, DerefMut},
use std::{fmt, io, time}; pin::Pin,
task::{Context, Poll},
time,
};
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
use actix_rt::task::JoinHandle; use actix_rt::task::JoinHandle;
use bytes::Bytes; use bytes::Bytes;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use h2::client::SendRequest; use h2::client::SendRequest;
use pin_project::pin_project;
use crate::body::MessageBody; use crate::body::MessageBody;
use crate::h1::ClientCodec; use crate::h1::ClientCodec;
@ -19,28 +21,148 @@ use super::error::SendRequestError;
use super::pool::Acquired; use super::pool::Acquired;
use super::{h1proto, h2proto}; use super::{h1proto, h2proto};
pub(crate) enum ConnectionType<Io> { /// Trait alias for types impl [tokio::io::AsyncRead] and [tokio::io::AsyncWrite].
H1(Io), pub trait ConnectionIo: AsyncRead + AsyncWrite + Unpin + 'static {}
H2(H2Connection),
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> ConnectionIo for T {}
/// HTTP client connection
pub struct H1Connection<Io: ConnectionIo> {
io: Option<Io>,
created: time::Instant,
acquired: Acquired<Io>,
} }
/// `H2Connection` has two parts: `SendRequest` and `Connection`. impl<Io: ConnectionIo> H1Connection<Io> {
/// close or release the connection to pool based on flag input
pub(super) fn on_release(&mut self, keep_alive: bool) {
if keep_alive {
self.release();
} else {
self.close();
}
}
/// Close connection
fn close(&mut self) {
let io = self.io.take().unwrap();
self.acquired.close(ConnectionInnerType::H1(io));
}
/// Release this connection to the connection pool
fn release(&mut self) {
let io = self.io.take().unwrap();
self.acquired
.release(ConnectionInnerType::H1(io), self.created);
}
fn io_pin_mut(self: Pin<&mut Self>) -> Pin<&mut Io> {
Pin::new(self.get_mut().io.as_mut().unwrap())
}
}
impl<Io: ConnectionIo> AsyncRead for H1Connection<Io> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
self.io_pin_mut().poll_read(cx, buf)
}
}
impl<Io: ConnectionIo> AsyncWrite for H1Connection<Io> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
self.io_pin_mut().poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.io_pin_mut().poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), io::Error>> {
self.io_pin_mut().poll_shutdown(cx)
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[io::IoSlice<'_>],
) -> Poll<io::Result<usize>> {
self.io_pin_mut().poll_write_vectored(cx, bufs)
}
fn is_write_vectored(&self) -> bool {
self.io.as_ref().unwrap().is_write_vectored()
}
}
/// HTTP2 client connection
pub struct H2Connection<Io: ConnectionIo> {
io: Option<H2ConnectionInner>,
created: time::Instant,
acquired: Acquired<Io>,
}
impl<Io: ConnectionIo> Deref for H2Connection<Io> {
type Target = SendRequest<Bytes>;
fn deref(&self) -> &Self::Target {
&self.io.as_ref().unwrap().sender
}
}
impl<Io: ConnectionIo> DerefMut for H2Connection<Io> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.io.as_mut().unwrap().sender
}
}
impl<Io: ConnectionIo> H2Connection<Io> {
/// close or release the connection to pool based on flag input
pub(super) fn on_release(&mut self, close: bool) {
if close {
self.close();
} else {
self.release();
}
}
/// Close connection
fn close(&mut self) {
let io = self.io.take().unwrap();
self.acquired.close(ConnectionInnerType::H2(io));
}
/// Release this connection to the connection pool
fn release(&mut self) {
let io = self.io.take().unwrap();
self.acquired
.release(ConnectionInnerType::H2(io), self.created);
}
}
/// `H2ConnectionInner` has two parts: `SendRequest` and `Connection`.
/// ///
/// `Connection` is spawned as an async task on runtime and `H2Connection` holds a handle for /// `Connection` is spawned as an async task on runtime and `H2ConnectionInner` holds a handle
/// this task. Therefore, it can wake up and quit the task when SendRequest is dropped. /// for this task. Therefore, it can wake up and quit the task when SendRequest is dropped.
pub(crate) struct H2Connection { pub(super) struct H2ConnectionInner {
handle: JoinHandle<()>, handle: JoinHandle<()>,
sender: SendRequest<Bytes>, sender: SendRequest<Bytes>,
} }
impl H2Connection { impl H2ConnectionInner {
pub(crate) fn new<Io>( pub(super) fn new<Io: ConnectionIo>(
sender: SendRequest<Bytes>, sender: SendRequest<Bytes>,
connection: h2::client::Connection<Io>, connection: h2::client::Connection<Io>,
) -> Self ) -> Self {
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
let handle = actix_rt::spawn(async move { let handle = actix_rt::spawn(async move {
let _ = connection.await; let _ = connection.await;
}); });
@ -49,161 +171,86 @@ impl H2Connection {
} }
} }
// cancel spawned connection task on drop. /// Cancel spawned connection task on drop.
impl Drop for H2Connection { impl Drop for H2ConnectionInner {
fn drop(&mut self) { fn drop(&mut self) {
self.handle.abort(); if self
} .sender
} .send_request(http::Request::new(()), true)
.is_err()
// only expose sender type to public. {
impl Deref for H2Connection { self.handle.abort();
type Target = SendRequest<Bytes>;
fn deref(&self) -> &Self::Target {
&self.sender
}
}
impl DerefMut for H2Connection {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sender
}
}
pub trait Connection {
type Io: AsyncRead + AsyncWrite + Unpin;
/// Send request and body
fn send_request<B, H>(
self,
head: H,
body: B,
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
where
B: MessageBody + 'static,
H: Into<RequestHeadType> + 'static;
/// Send request, returns Response and Framed
fn open_tunnel<H: Into<RequestHeadType> + 'static>(
self,
head: H,
) -> LocalBoxFuture<
'static,
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
>;
}
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
/// Close connection
fn close(self: Pin<&mut Self>);
/// Release connection to the connection pool
fn release(self: Pin<&mut Self>);
}
#[doc(hidden)]
/// HTTP client connection
pub struct IoConnection<T>
where
T: AsyncWrite + Unpin + 'static,
{
io: Option<ConnectionType<T>>,
created: time::Instant,
pool: Option<Acquired<T>>,
}
impl<T> fmt::Debug for IoConnection<T>
where
T: AsyncWrite + Unpin + fmt::Debug + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.io {
Some(ConnectionType::H1(ref io)) => write!(f, "H1Connection({:?})", io),
Some(ConnectionType::H2(_)) => write!(f, "H2Connection"),
None => write!(f, "Connection(Empty)"),
}
}
}
impl<T: AsyncRead + AsyncWrite + Unpin> IoConnection<T> {
pub(crate) fn new(
io: ConnectionType<T>,
created: time::Instant,
pool: Option<Acquired<T>>,
) -> Self {
IoConnection {
pool,
created,
io: Some(io),
}
}
pub(crate) fn into_inner(self) -> (ConnectionType<T>, time::Instant) {
(self.io.unwrap(), self.created)
}
#[cfg(test)]
pub(crate) fn into_parts(self) -> (ConnectionType<T>, time::Instant, Acquired<T>) {
(self.io.unwrap(), self.created, self.pool.unwrap())
}
async fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
mut self,
head: H,
body: B,
) -> Result<(ResponseHead, Payload), SendRequestError> {
match self.io.take().unwrap() {
ConnectionType::H1(io) => {
h1proto::send_request(io, head.into(), body, self.created, self.pool)
.await
}
ConnectionType::H2(io) => {
h2proto::send_request(io, head.into(), body, self.created, self.pool)
.await
}
}
}
/// Send request, returns Response and Framed
async fn open_tunnel<H: Into<RequestHeadType>>(
mut self,
head: H,
) -> Result<(ResponseHead, Framed<T, ClientCodec>), SendRequestError> {
match self.io.take().unwrap() {
ConnectionType::H1(io) => h1proto::open_tunnel(io, head.into()).await,
ConnectionType::H2(io) => {
if let Some(mut pool) = self.pool.take() {
pool.release(IoConnection::new(
ConnectionType::H2(io),
self.created,
None,
));
}
Err(SendRequestError::TunnelNotSupported)
}
} }
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) enum EitherIoConnection<A, B> /// Unified connection type cover Http1 Plain/Tls and Http2 protocols
pub enum Connection<A, B = Box<dyn ConnectionIo>>
where where
A: AsyncRead + AsyncWrite + Unpin + 'static, A: ConnectionIo,
B: AsyncRead + AsyncWrite + Unpin + 'static, B: ConnectionIo,
{ {
A(IoConnection<A>), Tcp(ConnectionType<A>),
B(IoConnection<B>), Tls(ConnectionType<B>),
} }
impl<A, B> Connection for EitherIoConnection<A, B> /// Unified connection type cover Http1/2 protocols
where pub enum ConnectionType<Io: ConnectionIo> {
A: AsyncRead + AsyncWrite + Unpin + 'static, H1(H1Connection<Io>),
B: AsyncRead + AsyncWrite + Unpin + 'static, H2(H2Connection<Io>),
{ }
type Io = EitherIo<A, B>;
fn send_request<RB, H>( /// Helper type for storing connection types in pool.
pub(super) enum ConnectionInnerType<Io> {
H1(Io),
H2(H2ConnectionInner),
}
impl<Io: ConnectionIo> ConnectionType<Io> {
pub(super) fn from_pool(
inner: ConnectionInnerType<Io>,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
match inner {
ConnectionInnerType::H1(io) => Self::from_h1(io, created, acquired),
ConnectionInnerType::H2(io) => Self::from_h2(io, created, acquired),
}
}
pub(super) fn from_h1(
io: Io,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
Self::H1(H1Connection {
io: Some(io),
created,
acquired,
})
}
pub(super) fn from_h2(
io: H2ConnectionInner,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
Self::H2(H2Connection {
io: Some(io),
created,
acquired,
})
}
}
impl<A, B> Connection<A, B>
where
A: ConnectionIo,
B: ConnectionIo,
{
/// Send a request through connection.
pub fn send_request<RB, H>(
self, self,
head: H, head: H,
body: RB, body: RB,
@ -212,76 +259,106 @@ where
RB: MessageBody + 'static, RB: MessageBody + 'static,
H: Into<RequestHeadType> + 'static, H: Into<RequestHeadType> + 'static,
{ {
match self { Box::pin(async move {
EitherIoConnection::A(con) => Box::pin(con.send_request(head, body)), match self {
EitherIoConnection::B(con) => Box::pin(con.send_request(head, body)), Connection::Tcp(ConnectionType::H1(conn)) => {
} h1proto::send_request(conn, head.into(), body).await
}
Connection::Tls(ConnectionType::H1(conn)) => {
h1proto::send_request(conn, head.into(), body).await
}
Connection::Tls(ConnectionType::H2(conn)) => {
h2proto::send_request(conn, head.into(), body).await
}
_ => unreachable!(
"Plain Tcp connection can be used only in Http1 protocol"
),
}
})
} }
/// Send request, returns Response and Framed /// Send request, returns Response and Framed tunnel.
fn open_tunnel<H: Into<RequestHeadType> + 'static>( pub fn open_tunnel<H: Into<RequestHeadType> + 'static>(
self, self,
head: H, head: H,
) -> LocalBoxFuture< ) -> LocalBoxFuture<
'static, 'static,
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>, Result<(ResponseHead, Framed<Connection<A, B>, ClientCodec>), SendRequestError>,
> { > {
match self { Box::pin(async move {
EitherIoConnection::A(con) => Box::pin(async { match self {
let (head, framed) = con.open_tunnel(head).await?; Connection::Tcp(ConnectionType::H1(ref _conn)) => {
Ok((head, framed.into_map_io(EitherIo::A))) let (head, framed) = h1proto::open_tunnel(self, head.into()).await?;
}), Ok((head, framed))
EitherIoConnection::B(con) => Box::pin(async { }
let (head, framed) = con.open_tunnel(head).await?; Connection::Tls(ConnectionType::H1(ref _conn)) => {
Ok((head, framed.into_map_io(EitherIo::B))) let (head, framed) = h1proto::open_tunnel(self, head.into()).await?;
}), Ok((head, framed))
} }
Connection::Tls(ConnectionType::H2(mut conn)) => {
conn.release();
Err(SendRequestError::TunnelNotSupported)
}
Connection::Tcp(ConnectionType::H2(_)) => {
unreachable!(
"Plain Tcp connection can be used only in Http1 protocol"
)
}
}
})
} }
} }
#[pin_project(project = EitherIoProj)] impl<A, B> AsyncRead for Connection<A, B>
pub enum EitherIo<A, B> {
A(#[pin] A),
B(#[pin] B),
}
impl<A, B> AsyncRead for EitherIo<A, B>
where where
A: AsyncRead, A: ConnectionIo,
B: AsyncRead, B: ConnectionIo,
{ {
fn poll_read( fn poll_read(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>, buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> { ) -> Poll<io::Result<()>> {
match self.project() { match self.get_mut() {
EitherIoProj::A(val) => val.poll_read(cx, buf), Connection::Tcp(ConnectionType::H1(conn)) => {
EitherIoProj::B(val) => val.poll_read(cx, buf), Pin::new(conn).poll_read(cx, buf)
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_read(cx, buf)
}
_ => unreachable!("H2Connection can not impl AsyncRead trait"),
} }
} }
} }
impl<A, B> AsyncWrite for EitherIo<A, B> const H2_UNREACHABLE_WRITE: &str = "H2Connection can not impl AsyncWrite trait";
impl<A, B> AsyncWrite for Connection<A, B>
where where
A: AsyncWrite, A: ConnectionIo,
B: AsyncWrite, B: ConnectionIo,
{ {
fn poll_write( fn poll_write(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &[u8], buf: &[u8],
) -> Poll<io::Result<usize>> { ) -> Poll<io::Result<usize>> {
match self.project() { match self.get_mut() {
EitherIoProj::A(val) => val.poll_write(cx, buf), Connection::Tcp(ConnectionType::H1(conn)) => {
EitherIoProj::B(val) => val.poll_write(cx, buf), Pin::new(conn).poll_write(cx, buf)
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_write(cx, buf)
}
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self.project() { match self.get_mut() {
EitherIoProj::A(val) => val.poll_flush(cx), Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx),
EitherIoProj::B(val) => val.poll_flush(cx), Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx),
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
@ -289,18 +366,56 @@ where
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<io::Result<()>> { ) -> Poll<io::Result<()>> {
match self.project() { match self.get_mut() {
EitherIoProj::A(val) => val.poll_shutdown(cx), Connection::Tcp(ConnectionType::H1(conn)) => {
EitherIoProj::B(val) => val.poll_shutdown(cx), Pin::new(conn).poll_shutdown(cx)
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_shutdown(cx)
}
_ => unreachable!(H2_UNREACHABLE_WRITE),
}
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[io::IoSlice<'_>],
) -> Poll<io::Result<usize>> {
match self.get_mut() {
Connection::Tcp(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_write_vectored(cx, bufs)
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_write_vectored(cx, bufs)
}
_ => unreachable!(H2_UNREACHABLE_WRITE),
}
}
fn is_write_vectored(&self) -> bool {
match *self {
Connection::Tcp(ConnectionType::H1(ref conn)) => conn.is_write_vectored(),
Connection::Tls(ConnectionType::H1(ref conn)) => conn.is_write_vectored(),
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::net; use std::{
future::Future,
net,
pin::Pin,
task::{Context, Poll},
time::{Duration, Instant},
};
use actix_rt::net::TcpStream; use actix_rt::{
net::TcpStream,
time::{interval, Interval},
};
use super::*; use super::*;
@ -314,16 +429,46 @@ mod test {
let tcp = TcpStream::connect(local).await.unwrap(); let tcp = TcpStream::connect(local).await.unwrap();
let (sender, connection) = h2::client::handshake(tcp).await.unwrap(); let (sender, connection) = h2::client::handshake(tcp).await.unwrap();
let conn = H2Connection::new(sender.clone(), connection); let conn = H2ConnectionInner::new(sender.clone(), connection);
assert!(sender.clone().ready().await.is_ok()); assert!(sender.clone().ready().await.is_ok());
assert!(h2::client::SendRequest::clone(&*conn).ready().await.is_ok()); assert!(h2::client::SendRequest::clone(&conn.sender)
.ready()
.await
.is_ok());
drop(conn); drop(conn);
match sender.ready().await { struct DropCheck {
Ok(_) => panic!("connection should be gone and can not be ready"), sender: h2::client::SendRequest<Bytes>,
Err(e) => assert!(e.is_io()), interval: Interval,
}; start_from: Instant,
}
impl Future for DropCheck {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
match futures_core::ready!(this.sender.poll_ready(cx)) {
Ok(()) => {
if this.start_from.elapsed() > Duration::from_secs(10) {
panic!("connection should be gone and can not be ready");
} else {
let _ = this.interval.poll_tick(cx);
Poll::Pending
}
}
Err(_) => Poll::Ready(()),
}
}
}
DropCheck {
sender,
interval: interval(Duration::from_millis(100)),
start_from: Instant::now(),
}
.await;
} }
} }

View File

@ -1,51 +1,53 @@
use std::{ use std::{
fmt, fmt,
future::Future, future::Future,
marker::PhantomData,
net::IpAddr, net::IpAddr,
pin::Pin, pin::Pin,
rc::Rc,
task::{Context, Poll}, task::{Context, Poll},
time::Duration, time::Duration,
}; };
use actix_codec::{AsyncRead, AsyncWrite}; use actix_rt::{
use actix_rt::net::TcpStream; net::{ActixStream, TcpStream},
use actix_service::{apply_fn, Service, ServiceExt}; time::{sleep, Sleep},
use actix_tls::connect::{
new_connector, Connect as TcpConnect, Connection as TcpConnection, Resolver,
}; };
use actix_utils::timeout::{TimeoutError, TimeoutService}; use actix_service::Service;
use actix_tls::connect::{
new_connector, Connect as TcpConnect, ConnectError as TcpConnectError,
Connection as TcpConnection, Resolver,
};
use futures_core::{future::LocalBoxFuture, ready};
use http::Uri; use http::Uri;
use pin_project::pin_project;
use super::config::ConnectorConfig; use super::config::ConnectorConfig;
use super::connection::{Connection, EitherIoConnection}; use super::connection::{Connection, ConnectionIo};
use super::error::ConnectError; use super::error::ConnectError;
use super::pool::{ConnectionPool, Protocol}; use super::pool::ConnectionPool;
use super::Connect; use super::Connect;
use super::Protocol;
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector; use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use actix_tls::connect::ssl::rustls::ClientConfig; use actix_tls::connect::ssl::rustls::ClientConfig;
#[cfg(feature = "rustls")]
use std::sync::Arc;
#[cfg(any(feature = "openssl", feature = "rustls"))]
enum SslConnector { enum SslConnector {
#[allow(dead_code)]
None,
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
Openssl(OpensslConnector), Openssl(OpensslConnector),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
Rustls(Arc<ClientConfig>), Rustls(std::sync::Arc<ClientConfig>),
} }
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
type SslConnector = ();
/// Manages HTTP client network connectivity. /// Manages HTTP client network connectivity.
/// ///
/// The `Connector` type uses a builder-like combinator pattern for service /// The `Connector` type uses a builder-like combinator pattern for service
/// construction that finishes by calling the `.finish()` method. /// construction that finishes by calling the `.finish()` method.
/// ///
/// ```rust,ignore /// ```ignore
/// use std::time::Duration; /// use std::time::Duration;
/// use actix_http::client::Connector; /// use actix_http::client::Connector;
/// ///
@ -53,18 +55,14 @@ type SslConnector = ();
/// .timeout(Duration::from_secs(5)) /// .timeout(Duration::from_secs(5))
/// .finish(); /// .finish();
/// ``` /// ```
pub struct Connector<T, U> { pub struct Connector<T> {
connector: T, connector: T,
config: ConnectorConfig, config: ConnectorConfig,
#[allow(dead_code)] #[allow(dead_code)]
ssl: SslConnector, ssl: SslConnector,
_phantom: PhantomData<U>,
} }
pub trait Io: AsyncRead + AsyncWrite + Unpin {} impl Connector<()> {
impl<T: AsyncRead + AsyncWrite + Unpin> Io for T {}
impl Connector<(), ()> {
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)] #[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
pub fn new() -> Connector< pub fn new() -> Connector<
impl Service< impl Service<
@ -72,13 +70,11 @@ impl Connector<(), ()> {
Response = TcpConnection<Uri, TcpStream>, Response = TcpConnection<Uri, TcpStream>,
Error = actix_tls::connect::ConnectError, Error = actix_tls::connect::ConnectError,
> + Clone, > + Clone,
TcpStream,
> { > {
Connector { Connector {
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
connector: new_connector(resolver::resolver()), connector: new_connector(resolver::resolver()),
config: ConnectorConfig::default(), config: ConnectorConfig::default(),
_phantom: PhantomData,
} }
} }
@ -109,51 +105,66 @@ impl Connector<(), ()> {
config.root_store.add_server_trust_anchors( config.root_store.add_server_trust_anchors(
&actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS, &actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS,
); );
SslConnector::Rustls(Arc::new(config)) SslConnector::Rustls(std::sync::Arc::new(config))
} }
// ssl turned off, provides empty ssl connector // ssl turned off, provides empty ssl connector
#[cfg(not(any(feature = "openssl", feature = "rustls")))] #[cfg(not(any(feature = "openssl", feature = "rustls")))]
fn build_ssl(_: Vec<Vec<u8>>) -> SslConnector {} fn build_ssl(_: Vec<Vec<u8>>) -> SslConnector {
SslConnector::None
}
} }
impl<T, U> Connector<T, U> { impl<S> Connector<S> {
/// Use custom connector. /// Use custom connector.
pub fn connector<T1, U1>(self, connector: T1) -> Connector<T1, U1> pub fn connector<S1, Io1>(self, connector: S1) -> Connector<S1>
where where
U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug, Io1: ActixStream + fmt::Debug + 'static,
T1: Service< S1: Service<
TcpConnect<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, U1>, Response = TcpConnection<Uri, Io1>,
Error = actix_tls::connect::ConnectError, Error = TcpConnectError,
> + Clone, > + Clone,
{ {
Connector { Connector {
connector, connector,
config: self.config, config: self.config,
ssl: self.ssl, ssl: self.ssl,
_phantom: PhantomData,
} }
} }
} }
impl<T, U> Connector<T, U> impl<S, Io> Connector<S>
where where
U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static, // Note:
T: Service< // Input Io type is bound to ActixStream trait but internally in client module they
// are bound to ConnectionIo trait alias. And latter is the trait exposed to public
// in the form of Box<dyn ConnectionIo> type.
//
// This remap is to hide ActixStream's trait methods. They are not meant to be called
// from user code.
Io: ActixStream + fmt::Debug + 'static,
S: Service<
TcpConnect<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, U>, Response = TcpConnection<Uri, Io>,
Error = actix_tls::connect::ConnectError, Error = TcpConnectError,
> + Clone > + Clone
+ 'static, + 'static,
{ {
/// Connection timeout, i.e. max time to connect to remote host including dns name resolution. /// Tcp connection timeout, i.e. max time to connect to remote host including dns name
/// Set to 1 second by default. /// resolution. Set to 5 second by default.
pub fn timeout(mut self, timeout: Duration) -> Self { pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout; self.config.timeout = timeout;
self self
} }
/// Tls handshake timeout, i.e. max time to do tls handshake with remote host after tcp
/// connection established. Set to 5 second by default.
pub fn handshake_timeout(mut self, timeout: Duration) -> Self {
self.config.handshake_timeout = timeout;
self
}
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
/// Use custom `SslConnector` instance. /// Use custom `SslConnector` instance.
pub fn ssl(mut self, connector: OpensslConnector) -> Self { pub fn ssl(mut self, connector: OpensslConnector) -> Self {
@ -162,7 +173,8 @@ where
} }
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
pub fn rustls(mut self, connector: Arc<ClientConfig>) -> Self { /// Use custom `SslConnector` instance.
pub fn rustls(mut self, connector: std::sync::Arc<ClientConfig>) -> Self {
self.ssl = SslConnector::Rustls(connector); self.ssl = SslConnector::Rustls(connector);
self self
} }
@ -252,214 +264,422 @@ where
/// Finish configuration process and create connector service. /// Finish configuration process and create connector service.
/// The Connector builder always concludes by calling `finish()` last in /// The Connector builder always concludes by calling `finish()` last in
/// its combinator chain. /// its combinator chain.
pub fn finish( pub fn finish(self) -> ConnectorService<S, Io> {
self,
) -> impl Service<Connect, Response = impl Connection, Error = ConnectError> + Clone
{
let local_address = self.config.local_address; let local_address = self.config.local_address;
let timeout = self.config.timeout; let timeout = self.config.timeout;
let tcp_service = TimeoutService::new( let tcp_service_inner =
timeout, TcpConnectorInnerService::new(self.connector, timeout, local_address);
apply_fn(self.connector.clone(), move |msg: Connect, srv| {
let mut req = TcpConnect::new(msg.uri).set_addr(msg.addr);
if let Some(local_addr) = local_address { #[allow(clippy::redundant_clone)]
req = req.set_local_addr(local_addr); let tcp_service = TcpConnectorService {
service: tcp_service_inner.clone(),
};
let tls_service = match self.ssl {
SslConnector::None => None,
#[cfg(feature = "openssl")]
SslConnector::Openssl(tls) => {
const H2: &[u8] = b"h2";
use actix_tls::connect::ssl::openssl::{OpensslConnector, SslStream};
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, SslStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
.ssl()
.selected_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock), Protocol::Http2)
} else {
(Box::new(sock), Protocol::Http1)
}
}
} }
srv.call(req) let handshake_timeout = self.config.handshake_timeout;
})
.map_err(ConnectError::from)
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
)
.map_err(|e| match e {
TimeoutError::Service(e) => e,
TimeoutError::Timeout => ConnectError::Timeout,
});
#[cfg(not(any(feature = "openssl", feature = "rustls")))] let tls_service = TlsConnectorService {
{ tcp_service: tcp_service_inner,
// A dummy service for annotate tls pool's type signature. tls_service: OpensslConnector::service(tls),
pub type DummyService = Box< timeout: handshake_timeout,
dyn Service< };
Connect,
Response = (Box<dyn Io>, Protocol),
Error = ConnectError,
Future = futures_core::future::LocalBoxFuture<
'static,
Result<(Box<dyn Io>, Protocol), ConnectError>,
>,
>,
>;
InnerConnector::<_, DummyService, _, Box<dyn Io>> { Some(actix_service::boxed::rc_service(tls_service))
tcp_pool: ConnectionPool::new(
tcp_service,
self.config.no_disconnect_timeout(),
),
tls_pool: None,
} }
}
#[cfg(any(feature = "openssl", feature = "rustls"))]
{
const H2: &[u8] = b"h2";
use actix_service::{boxed::service, pipeline};
#[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::OpensslConnector;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use actix_tls::connect::ssl::rustls::{RustlsConnector, Session}; SslConnector::Rustls(tls) => {
const H2: &[u8] = b"h2";
let ssl_service = TimeoutService::new( use actix_tls::connect::ssl::rustls::{
timeout, RustlsConnector, Session, TlsStream,
pipeline( };
apply_fn(self.connector.clone(), move |msg: Connect, srv| {
let mut req = TcpConnect::new(msg.uri).set_addr(msg.addr);
if let Some(local_addr) = local_address { impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, TlsStream<Io>> {
req = req.set_local_addr(local_addr); fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
.get_ref()
.1
.get_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock), Protocol::Http2)
} else {
(Box::new(sock), Protocol::Http1)
} }
}
}
srv.call(req) let handshake_timeout = self.config.handshake_timeout;
})
.map_err(ConnectError::from),
)
.and_then(match self.ssl {
#[cfg(feature = "openssl")]
SslConnector::Openssl(ssl) => service(
OpensslConnector::service(ssl)
.map(|stream| {
let sock = stream.into_parts().0;
let h2 = sock
.ssl()
.selected_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock) as Box<dyn Io>, Protocol::Http2)
} else {
(Box::new(sock) as Box<dyn Io>, Protocol::Http1)
}
})
.map_err(ConnectError::from),
),
#[cfg(feature = "rustls")]
SslConnector::Rustls(ssl) => service(
RustlsConnector::service(ssl)
.map_err(ConnectError::from)
.map(|stream| {
let sock = stream.into_parts().0;
let h2 = sock
.get_ref()
.1
.get_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock) as Box<dyn Io>, Protocol::Http2)
} else {
(Box::new(sock) as Box<dyn Io>, Protocol::Http1)
}
}),
),
}),
)
.map_err(|e| match e {
TimeoutError::Service(e) => e,
TimeoutError::Timeout => ConnectError::Timeout,
});
InnerConnector { let tls_service = TlsConnectorService {
tcp_pool: ConnectionPool::new( tcp_service: tcp_service_inner,
tcp_service, tls_service: RustlsConnector::service(tls),
self.config.no_disconnect_timeout(), timeout: handshake_timeout,
), };
tls_pool: Some(ConnectionPool::new(ssl_service, self.config)),
Some(actix_service::boxed::rc_service(tls_service))
} }
};
let tcp_config = self.config.no_disconnect_timeout();
let tcp_pool = ConnectionPool::new(tcp_service, tcp_config);
let tls_config = self.config;
let tls_pool = tls_service
.map(move |tls_service| ConnectionPool::new(tls_service, tls_config));
ConnectorServicePriv { tcp_pool, tls_pool }
}
}
/// tcp service for map `TcpConnection<Uri, Io>` type to `(Io, Protocol)`
#[derive(Clone)]
pub struct TcpConnectorService<S: Clone> {
service: S,
}
impl<S, Io> Service<Connect> for TcpConnectorService<S>
where
S: Service<Connect, Response = TcpConnection<Uri, Io>, Error = ConnectError>
+ Clone
+ 'static,
{
type Response = (Io, Protocol);
type Error = ConnectError;
type Future = TcpConnectorFuture<S::Future>;
actix_service::forward_ready!(service);
fn call(&self, req: Connect) -> Self::Future {
TcpConnectorFuture {
fut: self.service.call(req),
} }
} }
} }
struct InnerConnector<S1, S2, Io1, Io2> #[pin_project]
pub struct TcpConnectorFuture<Fut> {
#[pin]
fut: Fut,
}
impl<Fut, Io> Future for TcpConnectorFuture<Fut>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static, Fut: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static, {
Io1: AsyncRead + AsyncWrite + Unpin + 'static, type Output = Result<(Io, Protocol), ConnectError>;
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project()
.fut
.poll(cx)
.map_ok(|res| (res.into_parts().0, Protocol::Http1))
}
}
/// service for establish tcp connection and do client tls handshake.
/// operation is canceled when timeout limit reached.
struct TlsConnectorService<S, St> {
/// tcp connection is canceled on `TcpConnectorInnerService`'s timeout setting.
tcp_service: S,
/// tls connection is canceled on `TlsConnectorService`'s timeout setting.
tls_service: St,
timeout: Duration,
}
impl<S, St, Io> Service<Connect> for TlsConnectorService<S, St>
where
S: Service<Connect, Response = TcpConnection<Uri, Io>, Error = ConnectError>
+ Clone
+ 'static,
St: Service<TcpConnection<Uri, Io>, Error = std::io::Error> + Clone + 'static,
Io: ConnectionIo,
St::Response: IntoConnectionIo,
{
type Response = (Box<dyn ConnectionIo>, Protocol);
type Error = ConnectError;
type Future = TlsConnectorFuture<St, S::Future, St::Future>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(self.tcp_service.poll_ready(cx))?;
ready!(self.tls_service.poll_ready(cx))?;
Poll::Ready(Ok(()))
}
fn call(&self, req: Connect) -> Self::Future {
let fut = self.tcp_service.call(req);
let tls_service = self.tls_service.clone();
let timeout = self.timeout;
TlsConnectorFuture::TcpConnect {
fut,
tls_service: Some(tls_service),
timeout,
}
}
}
#[pin_project(project = TlsConnectorProj)]
#[allow(clippy::large_enum_variant)]
enum TlsConnectorFuture<S, Fut1, Fut2> {
TcpConnect {
#[pin]
fut: Fut1,
tls_service: Option<S>,
timeout: Duration,
},
TlsConnect {
#[pin]
fut: Fut2,
#[pin]
timeout: Sleep,
},
}
/// helper trait for generic over different TlsStream types between tls crates.
trait IntoConnectionIo {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol);
}
impl<S, Io, Fut1, Fut2, Res> Future for TlsConnectorFuture<S, Fut1, Fut2>
where
S: Service<
TcpConnection<Uri, Io>,
Response = Res,
Error = std::io::Error,
Future = Fut2,
>,
S::Response: IntoConnectionIo,
Fut1: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>,
Fut2: Future<Output = Result<S::Response, S::Error>>,
Io: ConnectionIo,
{
type Output = Result<(Box<dyn ConnectionIo>, Protocol), ConnectError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project() {
TlsConnectorProj::TcpConnect {
fut,
tls_service,
timeout,
} => {
let res = ready!(fut.poll(cx))?;
let fut = tls_service
.take()
.expect("TlsConnectorFuture polled after complete")
.call(res);
let timeout = sleep(*timeout);
self.set(TlsConnectorFuture::TlsConnect { fut, timeout });
self.poll(cx)
}
TlsConnectorProj::TlsConnect { fut, timeout } => match fut.poll(cx)? {
Poll::Ready(res) => Poll::Ready(Ok(res.into_connection_io())),
Poll::Pending => timeout.poll(cx).map(|_| Err(ConnectError::Timeout)),
},
}
}
}
/// service for establish tcp connection.
/// operation is canceled when timeout limit reached.
#[derive(Clone)]
pub struct TcpConnectorInnerService<S: Clone> {
service: S,
timeout: Duration,
local_address: Option<std::net::IpAddr>,
}
impl<S: Clone> TcpConnectorInnerService<S> {
fn new(
service: S,
timeout: Duration,
local_address: Option<std::net::IpAddr>,
) -> Self {
Self {
service,
timeout,
local_address,
}
}
}
impl<S, Io> Service<Connect> for TcpConnectorInnerService<S>
where
S: Service<
TcpConnect<Uri>,
Response = TcpConnection<Uri, Io>,
Error = TcpConnectError,
> + Clone
+ 'static,
{
type Response = S::Response;
type Error = ConnectError;
type Future = TcpConnectorInnerFuture<S::Future>;
actix_service::forward_ready!(service);
fn call(&self, req: Connect) -> Self::Future {
let mut req = TcpConnect::new(req.uri).set_addr(req.addr);
if let Some(local_addr) = self.local_address {
req = req.set_local_addr(local_addr);
}
TcpConnectorInnerFuture {
fut: self.service.call(req),
timeout: sleep(self.timeout),
}
}
}
#[pin_project]
pub struct TcpConnectorInnerFuture<Fut> {
#[pin]
fut: Fut,
#[pin]
timeout: Sleep,
}
impl<Fut, Io> Future for TcpConnectorInnerFuture<Fut>
where
Fut: Future<Output = Result<TcpConnection<Uri, Io>, TcpConnectError>>,
{
type Output = Result<TcpConnection<Uri, Io>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this.fut.poll(cx) {
Poll::Ready(res) => Poll::Ready(res.map_err(ConnectError::from)),
Poll::Pending => this.timeout.poll(cx).map(|_| Err(ConnectError::Timeout)),
}
}
}
/// Connector service for pooled Plain/Tls Tcp connections.
pub type ConnectorService<S, Io> = ConnectorServicePriv<
TcpConnectorService<TcpConnectorInnerService<S>>,
Rc<
dyn Service<
Connect,
Response = (Box<dyn ConnectionIo>, Protocol),
Error = ConnectError,
Future = LocalBoxFuture<
'static,
Result<(Box<dyn ConnectionIo>, Protocol), ConnectError>,
>,
>,
>,
Io,
Box<dyn ConnectionIo>,
>;
pub struct ConnectorServicePriv<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
Io1: ConnectionIo,
Io2: ConnectionIo,
{ {
tcp_pool: ConnectionPool<S1, Io1>, tcp_pool: ConnectionPool<S1, Io1>,
tls_pool: Option<ConnectionPool<S2, Io2>>, tls_pool: Option<ConnectionPool<S2, Io2>>,
} }
impl<S1, S2, Io1, Io2> Clone for InnerConnector<S1, S2, Io1, Io2> impl<S1, S2, Io1, Io2> Service<Connect> for ConnectorServicePriv<S1, S2, Io1, Io2>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static, S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static, + Clone
Io1: AsyncRead + AsyncWrite + Unpin + 'static, + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static, S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo,
Io2: ConnectionIo,
{ {
fn clone(&self) -> Self { type Response = Connection<Io1, Io2>;
InnerConnector {
tcp_pool: self.tcp_pool.clone(),
tls_pool: self.tls_pool.as_ref().cloned(),
}
}
}
impl<S1, S2, Io1, Io2> Service<Connect> for InnerConnector<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
type Response = EitherIoConnection<Io1, Io2>;
type Error = ConnectError; type Error = ConnectError;
type Future = InnerConnectorResponse<S1, S2, Io1, Io2>; type Future = ConnectorServiceFuture<S1, S2, Io1, Io2>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.tcp_pool.poll_ready(cx) ready!(self.tcp_pool.poll_ready(cx))?;
if let Some(ref tls_pool) = self.tls_pool {
ready!(tls_pool.poll_ready(cx))?;
}
Poll::Ready(Ok(()))
} }
fn call(&self, req: Connect) -> Self::Future { fn call(&self, req: Connect) -> Self::Future {
match req.uri.scheme_str() { match req.uri.scheme_str() {
Some("https") | Some("wss") => match self.tls_pool { Some("https") | Some("wss") => match self.tls_pool {
None => InnerConnectorResponse::SslIsNotSupported, None => ConnectorServiceFuture::SslIsNotSupported,
Some(ref pool) => InnerConnectorResponse::Io2(pool.call(req)), Some(ref pool) => ConnectorServiceFuture::Tls(pool.call(req)),
}, },
_ => InnerConnectorResponse::Io1(self.tcp_pool.call(req)), _ => ConnectorServiceFuture::Tcp(self.tcp_pool.call(req)),
} }
} }
} }
#[pin_project::pin_project(project = InnerConnectorProj)] #[pin_project(project = ConnectorServiceProj)]
enum InnerConnectorResponse<S1, S2, Io1, Io2> pub enum ConnectorServiceFuture<S1, S2, Io1, Io2>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static, S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static, + Clone
Io1: AsyncRead + AsyncWrite + Unpin + 'static, + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static, S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo,
Io2: ConnectionIo,
{ {
Io1(#[pin] <ConnectionPool<S1, Io1> as Service<Connect>>::Future), Tcp(#[pin] <ConnectionPool<S1, Io1> as Service<Connect>>::Future),
Io2(#[pin] <ConnectionPool<S2, Io2> as Service<Connect>>::Future), Tls(#[pin] <ConnectionPool<S2, Io2> as Service<Connect>>::Future),
SslIsNotSupported, SslIsNotSupported,
} }
impl<S1, S2, Io1, Io2> Future for InnerConnectorResponse<S1, S2, Io1, Io2> impl<S1, S2, Io1, Io2> Future for ConnectorServiceFuture<S1, S2, Io1, Io2>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static, S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static, + Clone
Io1: AsyncRead + AsyncWrite + Unpin + 'static, + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static, S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo,
Io2: ConnectionIo,
{ {
type Output = Result<EitherIoConnection<Io1, Io2>, ConnectError>; type Output = Result<Connection<Io1, Io2>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project() { match self.project() {
InnerConnectorProj::Io1(fut) => fut.poll(cx).map_ok(EitherIoConnection::A), ConnectorServiceProj::Tcp(fut) => fut.poll(cx).map_ok(Connection::Tcp),
InnerConnectorProj::Io2(fut) => fut.poll(cx).map_ok(EitherIoConnection::B), ConnectorServiceProj::Tls(fut) => fut.poll(cx).map_ok(Connection::Tls),
InnerConnectorProj::SslIsNotSupported => { ConnectorServiceProj::SslIsNotSupported => {
Poll::Ready(Err(ConnectError::SslIsNotSupported)) Poll::Ready(Err(ConnectError::SslIsNotSupported))
} }
} }

View File

@ -1,38 +1,36 @@
use std::io::Write; use std::{
use std::pin::Pin; io::Write,
use std::task::{Context, Poll}; pin::Pin,
use std::{io, time}; task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; use actix_codec::Framed;
use actix_utils::future::poll_fn;
use bytes::buf::BufMut; use bytes::buf::BufMut;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::Stream; use futures_core::{ready, Stream};
use futures_util::{future::poll_fn, SinkExt, StreamExt}; use futures_util::SinkExt as _;
use crate::error::PayloadError; use crate::error::PayloadError;
use crate::h1; use crate::h1;
use crate::header::HeaderMap;
use crate::http::{ use crate::http::{
header::{IntoHeaderValue, EXPECT, HOST}, header::{HeaderMap, IntoHeaderValue, EXPECT, HOST},
StatusCode, StatusCode,
}; };
use crate::message::{RequestHeadType, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::{Payload, PayloadStream}; use crate::payload::Payload;
use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; use super::connection::{ConnectionIo, H1Connection};
use super::error::{ConnectError, SendRequestError}; use super::error::{ConnectError, SendRequestError};
use super::pool::Acquired;
use crate::body::{BodySize, MessageBody}; use crate::body::{BodySize, MessageBody};
pub(crate) async fn send_request<T, B>( pub(crate) async fn send_request<Io, B>(
io: T, io: H1Connection<Io>,
mut head: RequestHeadType, mut head: RequestHeadType,
body: B, body: B,
created: time::Instant,
pool: Option<Acquired<T>>,
) -> Result<(ResponseHead, Payload), SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
{ {
// set request host header // set request host header
@ -42,9 +40,9 @@ where
if let Some(host) = head.as_ref().uri.host() { if let Some(host) = head.as_ref().uri.host() {
let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); let mut wrt = BytesMut::with_capacity(host.len() + 5).writer();
let _ = match head.as_ref().uri.port_u16() { match head.as_ref().uri.port_u16() {
None | Some(80) | Some(443) => write!(wrt, "{}", host), None | Some(80) | Some(443) => write!(wrt, "{}", host)?,
Some(port) => write!(wrt, "{}:{}", host, port), Some(port) => write!(wrt, "{}:{}", host, port)?,
}; };
match wrt.get_mut().split().freeze().try_into_value() { match wrt.get_mut().split().freeze().try_into_value() {
@ -62,12 +60,6 @@ where
} }
} }
let io = H1Connection {
created,
pool,
io: Some(io),
};
// create Framed and prepare sending request // create Framed and prepare sending request
let mut framed = Framed::new(io, h1::ClientCodec::default()); let mut framed = Framed::new(io, h1::ClientCodec::default());
@ -77,10 +69,8 @@ where
let is_expect = if head.as_ref().headers.contains_key(EXPECT) { let is_expect = if head.as_ref().headers.contains_key(EXPECT) {
match body.size() { match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => { BodySize::None | BodySize::Empty | BodySize::Sized(0) => {
let pin_framed = Pin::new(&mut framed); let keep_alive = framed.codec_ref().keepalive();
framed.io_mut().on_release(keep_alive);
let force_close = !pin_framed.codec_ref().keepalive();
release_connection(pin_framed, force_close);
// TODO: use a new variant or a new type better describing error violate // TODO: use a new variant or a new type better describing error violate
// `Requirements for clients` session of above RFC // `Requirements for clients` session of above RFC
@ -128,44 +118,41 @@ where
match pin_framed.codec_ref().message_type() { match pin_framed.codec_ref().message_type() {
h1::MessageType::None => { h1::MessageType::None => {
let force_close = !pin_framed.codec_ref().keepalive(); let keep_alive = pin_framed.codec_ref().keepalive();
release_connection(pin_framed, force_close); pin_framed.io_mut().on_release(keep_alive);
Ok((head, Payload::None)) Ok((head, Payload::None))
} }
_ => { _ => Ok((head, Payload::Stream(Box::pin(PlStream::new(framed))))),
let pl: PayloadStream = Box::pin(PlStream::new(framed));
Ok((head, pl.into()))
}
} }
} }
pub(crate) async fn open_tunnel<T>( pub(crate) async fn open_tunnel<Io>(
io: T, io: Io,
head: RequestHeadType, head: RequestHeadType,
) -> Result<(ResponseHead, Framed<T, h1::ClientCodec>), SendRequestError> ) -> Result<(ResponseHead, Framed<Io, h1::ClientCodec>), SendRequestError>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, Io: ConnectionIo,
{ {
// create Framed and send request // create Framed and send request.
let mut framed = Framed::new(io, h1::ClientCodec::default()); let mut framed = Framed::new(io, h1::ClientCodec::default());
framed.send((head, BodySize::None).into()).await?; framed.send((head, BodySize::None).into()).await?;
// read response // read response head.
if let (Some(result), framed) = framed.into_future().await { let head = poll_fn(|cx| Pin::new(&mut framed).poll_next(cx))
let head = result.map_err(SendRequestError::from)?; .await
Ok((head, framed)) .ok_or(ConnectError::Disconnected)??;
} else {
Err(SendRequestError::from(ConnectError::Disconnected)) Ok((head, framed))
}
} }
/// send request body to the peer /// send request body to the peer
pub(crate) async fn send_body<T, B>( pub(crate) async fn send_body<Io, B>(
body: B, body: B,
mut framed: Pin<&mut Framed<T, h1::ClientCodec>>, mut framed: Pin<&mut Framed<Io, h1::ClientCodec>>,
) -> Result<(), SendRequestError> ) -> Result<(), SendRequestError>
where where
T: ConnectionLifetime + Unpin, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
{ {
actix_rt::pin!(body); actix_rt::pin!(body);
@ -200,104 +187,25 @@ where
} }
} }
SinkExt::flush(Pin::into_inner(framed)).await?; framed.get_mut().flush().await?;
Ok(()) Ok(())
} }
#[doc(hidden)]
/// HTTP client connection
pub struct H1Connection<T>
where
T: AsyncWrite + Unpin + 'static,
{
/// T should be `Unpin`
io: Option<T>,
created: time::Instant,
pool: Option<Acquired<T>>,
}
impl<T> ConnectionLifetime for H1Connection<T>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
{
/// Close connection
fn close(mut self: Pin<&mut Self>) {
if let Some(mut pool) = self.pool.take() {
if let Some(io) = self.io.take() {
pool.close(IoConnection::new(
ConnectionType::H1(io),
self.created,
None,
));
}
}
}
/// Release this connection to the connection pool
fn release(mut self: Pin<&mut Self>) {
if let Some(mut pool) = self.pool.take() {
if let Some(io) = self.io.take() {
pool.release(IoConnection::new(
ConnectionType::H1(io),
self.created,
None,
));
}
}
}
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncRead for H1Connection<T> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
}
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncWrite for H1Connection<T> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
Pin::new(self.io.as_mut().unwrap()).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), io::Error>> {
Pin::new(self.io.as_mut().unwrap()).poll_shutdown(cx)
}
}
#[pin_project::pin_project] #[pin_project::pin_project]
pub(crate) struct PlStream<Io> { pub(crate) struct PlStream<Io: ConnectionIo> {
#[pin] #[pin]
framed: Option<Framed<Io, h1::ClientPayloadCodec>>, framed: Framed<H1Connection<Io>, h1::ClientPayloadCodec>,
} }
impl<Io: ConnectionLifetime> PlStream<Io> { impl<Io: ConnectionIo> PlStream<Io> {
fn new(framed: Framed<Io, h1::ClientCodec>) -> Self { fn new(framed: Framed<H1Connection<Io>, h1::ClientCodec>) -> Self {
let framed = framed.into_map_codec(|codec| codec.into_payload_codec()); let framed = framed.into_map_codec(|codec| codec.into_payload_codec());
PlStream { PlStream { framed }
framed: Some(framed),
}
} }
} }
impl<Io: ConnectionLifetime> Stream for PlStream<Io> { impl<Io: ConnectionIo> Stream for PlStream<Io> {
type Item = Result<Bytes, PayloadError>; type Item = Result<Bytes, PayloadError>;
fn poll_next( fn poll_next(
@ -306,30 +214,14 @@ impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
let mut this = self.project(); let mut this = self.project();
match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? { match ready!(this.framed.as_mut().next_item(cx)?) {
Poll::Pending => Poll::Pending, Some(Some(chunk)) => Poll::Ready(Some(Ok(chunk))),
Poll::Ready(Some(chunk)) => { Some(None) => {
if let Some(chunk) = chunk { let keep_alive = this.framed.codec_ref().keepalive();
Poll::Ready(Some(Ok(chunk))) this.framed.io_mut().on_release(keep_alive);
} else { Poll::Ready(None)
let framed = this.framed.as_mut().as_pin_mut().unwrap();
let force_close = !framed.codec_ref().keepalive();
release_connection(framed, force_close);
Poll::Ready(None)
}
} }
Poll::Ready(None) => Poll::Ready(None), None => Poll::Ready(None),
} }
} }
} }
fn release_connection<T, U>(framed: Pin<&mut Framed<T, U>>, force_close: bool)
where
T: ConnectionLifetime,
{
if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() {
framed.io_pin().release()
} else {
framed.io_pin().close()
}
}

View File

@ -1,9 +1,7 @@
use std::future::Future; use std::future::Future;
use std::time;
use actix_codec::{AsyncRead, AsyncWrite}; use actix_utils::future::poll_fn;
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::poll_fn;
use h2::{ use h2::{
client::{Builder, Connection, SendRequest}, client::{Builder, Connection, SendRequest},
SendStream, SendStream,
@ -17,20 +15,16 @@ use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; use crate::payload::Payload;
use super::config::ConnectorConfig; use super::config::ConnectorConfig;
use super::connection::{ConnectionType, IoConnection}; use super::connection::{ConnectionIo, H2Connection};
use super::error::SendRequestError; use super::error::SendRequestError;
use super::pool::Acquired;
use crate::client::connection::H2Connection;
pub(crate) async fn send_request<T, B>( pub(crate) async fn send_request<Io, B>(
mut io: H2Connection, mut io: H2Connection<Io>,
head: RequestHeadType, head: RequestHeadType,
body: B, body: B,
created: time::Instant,
pool: Option<Acquired<T>>,
) -> Result<(ResponseHead, Payload), SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
{ {
trace!("Sending client request: {:?} {:?}", head, body.size()); trace!("Sending client request: {:?} {:?}", head, body.size());
@ -103,13 +97,13 @@ where
let res = poll_fn(|cx| io.poll_ready(cx)).await; let res = poll_fn(|cx| io.poll_ready(cx)).await;
if let Err(e) = res { if let Err(e) = res {
release(io, pool, created, e.is_io()); io.on_release(e.is_io());
return Err(SendRequestError::from(e)); return Err(SendRequestError::from(e));
} }
let resp = match io.send_request(req, eof) { let resp = match io.send_request(req, eof) {
Ok((fut, send)) => { Ok((fut, send)) => {
release(io, pool, created, false); io.on_release(false);
if !eof { if !eof {
send_body(body, send).await?; send_body(body, send).await?;
@ -117,7 +111,7 @@ where
fut.await.map_err(SendRequestError::from)? fut.await.map_err(SendRequestError::from)?
} }
Err(e) => { Err(e) => {
release(io, pool, created, e.is_io()); io.on_release(e.is_io());
return Err(e.into()); return Err(e.into());
} }
}; };
@ -178,28 +172,10 @@ async fn send_body<B: MessageBody>(
} }
} }
/// release SendRequest object pub(crate) fn handshake<Io: ConnectionIo>(
fn release<T: AsyncRead + AsyncWrite + Unpin + 'static>(
io: H2Connection,
pool: Option<Acquired<T>>,
created: time::Instant,
close: bool,
) {
if let Some(mut pool) = pool {
if close {
pool.close(IoConnection::new(ConnectionType::H2(io), created, None));
} else {
pool.release(IoConnection::new(ConnectionType::H2(io), created, None));
}
}
}
pub(crate) fn handshake<Io>(
io: Io, io: Io,
config: &ConnectorConfig, config: &ConnectorConfig,
) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>> ) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
let mut builder = Builder::new(); let mut builder = Builder::new();
builder builder

View File

@ -14,10 +14,10 @@ pub use actix_tls::connect::{
Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection, Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection,
}; };
pub use self::connection::Connection; pub use self::connection::{Connection, ConnectionIo};
pub use self::connector::Connector; pub use self::connector::{Connector, ConnectorService};
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
pub use self::pool::Protocol; pub use crate::Protocol;
#[derive(Clone)] #[derive(Clone)]
pub struct Connect { pub struct Connect {

View File

@ -1,40 +1,38 @@
//! Client connection pooling keyed on the authority part of the connection URI. //! Client connection pooling keyed on the authority part of the connection URI.
use std::collections::VecDeque; use std::{
use std::future::Future; cell::RefCell,
use std::ops::Deref; collections::VecDeque,
use std::pin::Pin; future::Future,
use std::rc::Rc; io,
use std::sync::Arc; ops::Deref,
use std::task::{Context, Poll}; pin::Pin,
use std::time::{Duration, Instant}; rc::Rc,
use std::{cell::RefCell, io}; sync::Arc,
task::{Context, Poll},
time::{Duration, Instant},
};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use actix_rt::time::{sleep, Sleep}; use actix_rt::time::{sleep, Sleep};
use actix_service::Service; use actix_service::Service;
use ahash::AHashMap; use ahash::AHashMap;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use http::uri::Authority; use http::uri::Authority;
use pin_project::pin_project; use pin_project::pin_project;
use tokio::io::ReadBuf;
use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use super::config::ConnectorConfig; use super::config::ConnectorConfig;
use super::connection::{ConnectionType, H2Connection, IoConnection}; use super::connection::{
ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner,
};
use super::error::ConnectError; use super::error::ConnectError;
use super::h2proto::handshake; use super::h2proto::handshake;
use super::Connect; use super::Connect;
use super::Protocol;
#[derive(Clone, Copy, PartialEq)]
/// Protocol version
pub enum Protocol {
Http1,
Http2,
}
#[derive(Hash, Eq, PartialEq, Clone, Debug)] #[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub(crate) struct Key { pub struct Key {
authority: Authority, authority: Authority,
} }
@ -44,17 +42,18 @@ impl From<Authority> for Key {
} }
} }
#[doc(hidden)]
/// Connections pool for reuse Io type for certain [`http::uri::Authority`] as key. /// Connections pool for reuse Io type for certain [`http::uri::Authority`] as key.
pub(crate) struct ConnectionPool<S, Io> pub struct ConnectionPool<S, Io>
where where
Io: AsyncWrite + Unpin + 'static, Io: AsyncWrite + Unpin + 'static,
{ {
connector: Rc<S>, connector: S,
inner: ConnectionPoolInner<Io>, inner: ConnectionPoolInner<Io>,
} }
/// wrapper type for check the ref count of Rc. /// wrapper type for check the ref count of Rc.
struct ConnectionPoolInner<Io>(Rc<ConnectionPoolInnerPriv<Io>>) pub struct ConnectionPoolInner<Io>(Rc<ConnectionPoolInnerPriv<Io>>)
where where
Io: AsyncWrite + Unpin + 'static; Io: AsyncWrite + Unpin + 'static;
@ -62,10 +61,21 @@ impl<Io> ConnectionPoolInner<Io>
where where
Io: AsyncWrite + Unpin + 'static, Io: AsyncWrite + Unpin + 'static,
{ {
fn new(config: ConnectorConfig) -> Self {
let permits = Arc::new(Semaphore::new(config.limit));
let available = RefCell::new(AHashMap::default());
Self(Rc::new(ConnectionPoolInnerPriv {
config,
available,
permits,
}))
}
/// spawn a async for graceful shutdown h1 Io type with a timeout. /// spawn a async for graceful shutdown h1 Io type with a timeout.
fn close(&self, conn: ConnectionType<Io>) { fn close(&self, conn: ConnectionInnerType<Io>) {
if let Some(timeout) = self.config.disconnect_timeout { if let Some(timeout) = self.config.disconnect_timeout {
if let ConnectionType::H1(io) = conn { if let ConnectionInnerType::H1(io) = conn {
actix_rt::spawn(CloseConnection::new(io, timeout)); actix_rt::spawn(CloseConnection::new(io, timeout));
} }
} }
@ -110,7 +120,7 @@ where
} }
} }
struct ConnectionPoolInnerPriv<Io> pub struct ConnectionPoolInnerPriv<Io>
where where
Io: AsyncWrite + Unpin + 'static, Io: AsyncWrite + Unpin + 'static,
{ {
@ -134,40 +144,22 @@ where
/// Any requests beyond limit would be wait in fifo order and get notified in async manner /// Any requests beyond limit would be wait in fifo order and get notified in async manner
/// by [`tokio::sync::Semaphore`] /// by [`tokio::sync::Semaphore`]
pub(crate) fn new(connector: S, config: ConnectorConfig) -> Self { pub(crate) fn new(connector: S, config: ConnectorConfig) -> Self {
let permits = Arc::new(Semaphore::new(config.limit)); let inner = ConnectionPoolInner::new(config);
let available = RefCell::new(AHashMap::default());
let connector = Rc::new(connector);
let inner = ConnectionPoolInner(Rc::new(ConnectionPoolInnerPriv {
config,
available,
permits,
}));
Self { connector, inner } Self { connector, inner }
} }
} }
impl<S, Io> Clone for ConnectionPool<S, Io>
where
Io: AsyncWrite + Unpin + 'static,
{
fn clone(&self) -> Self {
Self {
connector: self.connector.clone(),
inner: self.inner.clone(),
}
}
}
impl<S, Io> Service<Connect> for ConnectionPool<S, Io> impl<S, Io> Service<Connect> for ConnectionPool<S, Io>
where where
S: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static, S: Service<Connect, Response = (Io, Protocol), Error = ConnectError>
Io: AsyncRead + AsyncWrite + Unpin + 'static, + Clone
+ 'static,
Io: ConnectionIo,
{ {
type Response = IoConnection<Io>; type Response = ConnectionType<Io>;
type Error = ConnectError; type Error = ConnectError;
type Future = LocalBoxFuture<'static, Result<IoConnection<Io>, ConnectError>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::forward_ready!(connector); actix_service::forward_ready!(connector);
@ -211,7 +203,7 @@ where
inner.close(c.conn); inner.close(c.conn);
} else { } else {
// check if the connection is still usable // check if the connection is still usable
if let ConnectionType::H1(ref mut io) = c.conn { if let ConnectionInnerType::H1(ref mut io) = c.conn {
let check = ConnectionCheckFuture { io }; let check = ConnectionCheckFuture { io };
match check.await { match check.await {
ConnectionState::Tainted => { ConnectionState::Tainted => {
@ -235,28 +227,26 @@ where
// construct acquired. It's used to put Io type back to pool/ close the Io type. // construct acquired. It's used to put Io type back to pool/ close the Io type.
// permit is carried with the whole lifecycle of Acquired. // permit is carried with the whole lifecycle of Acquired.
let acquired = Some(Acquired { key, inner, permit }); let acquired = Acquired { key, inner, permit };
// match the connection and spawn new one if did not get anything. // match the connection and spawn new one if did not get anything.
match conn { match conn {
Some(conn) => Ok(IoConnection::new(conn.conn, conn.created, acquired)), Some(conn) => {
Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired))
}
None => { None => {
let (io, proto) = connector.call(req).await?; let (io, proto) = connector.call(req).await?;
// TODO: remove when http3 is added in support.
assert!(proto != Protocol::Http3);
if proto == Protocol::Http1 { if proto == Protocol::Http1 {
Ok(IoConnection::new( Ok(ConnectionType::from_h1(io, Instant::now(), acquired))
ConnectionType::H1(io),
Instant::now(),
acquired,
))
} else { } else {
let config = &acquired.as_ref().unwrap().inner.config; let config = &acquired.inner.config;
let (sender, connection) = handshake(io, config).await?; let (sender, connection) = handshake(io, config).await?;
Ok(IoConnection::new( let inner = H2ConnectionInner::new(sender, connection);
ConnectionType::H2(H2Connection::new(sender, connection)), Ok(ConnectionType::from_h2(inner, Instant::now(), acquired))
Instant::now(),
acquired,
))
} }
} }
} }
@ -307,7 +297,7 @@ where
} }
struct PooledConnection<Io> { struct PooledConnection<Io> {
conn: ConnectionType<Io>, conn: ConnectionInnerType<Io>,
used: Instant, used: Instant,
created: Instant, created: Instant,
} }
@ -347,28 +337,26 @@ where
} }
} }
pub(crate) struct Acquired<Io> pub struct Acquired<Io>
where where
Io: AsyncWrite + Unpin + 'static, Io: AsyncWrite + Unpin + 'static,
{ {
/// authority key for identify connection.
key: Key, key: Key,
/// handle to connection pool.
inner: ConnectionPoolInner<Io>, inner: ConnectionPoolInner<Io>,
/// permit for limit concurrent in-flight connection for a Client object.
permit: OwnedSemaphorePermit, permit: OwnedSemaphorePermit,
} }
impl<Io> Acquired<Io> impl<Io: ConnectionIo> Acquired<Io> {
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
/// Close the IO. /// Close the IO.
pub(crate) fn close(&mut self, conn: IoConnection<Io>) { pub(super) fn close(&self, conn: ConnectionInnerType<Io>) {
let (conn, _) = conn.into_inner();
self.inner.close(conn); self.inner.close(conn);
} }
/// Release IO back into pool. /// Release IO back into pool.
pub(crate) fn release(&mut self, conn: IoConnection<Io>) { pub(super) fn release(&self, conn: ConnectionInnerType<Io>, created: Instant) {
let (io, created) = conn.into_inner();
let Acquired { key, inner, .. } = self; let Acquired { key, inner, .. } = self;
inner inner
@ -377,12 +365,12 @@ where
.entry(key.clone()) .entry(key.clone())
.or_insert_with(VecDeque::new) .or_insert_with(VecDeque::new)
.push_back(PooledConnection { .push_back(PooledConnection {
conn: io, conn,
created, created,
used: Instant::now(), used: Instant::now(),
}); });
let _ = &mut self.permit; let _ = &self.permit;
} }
} }
@ -393,7 +381,7 @@ mod test {
use http::Uri; use http::Uri;
use super::*; use super::*;
use crate::client::connection::IoConnection; use crate::client::connection::ConnectionType;
/// A stream type that always returns pending on async read. /// A stream type that always returns pending on async read.
/// ///
@ -440,6 +428,7 @@ mod test {
} }
} }
#[derive(Clone)]
struct TestPoolConnector { struct TestPoolConnector {
generated: Rc<Cell<usize>>, generated: Rc<Cell<usize>>,
} }
@ -458,12 +447,14 @@ mod test {
} }
} }
fn release<T>(conn: IoConnection<T>) fn release<T>(conn: ConnectionType<T>)
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
let (conn, created, mut acquired) = conn.into_parts(); match conn {
acquired.release(IoConnection::new(conn, created, None)); ConnectionType::H1(mut conn) => conn.on_release(true),
ConnectionType::H2(mut conn) => conn.on_release(false),
}
} }
#[actix_rt::test] #[actix_rt::test]

View File

@ -126,9 +126,7 @@ impl ServiceConfig {
pub fn client_timer(&self) -> Option<Sleep> { pub fn client_timer(&self) -> Option<Sleep> {
let delay_time = self.0.client_timeout; let delay_time = self.0.client_timeout;
if delay_time != 0 { if delay_time != 0 {
Some(sleep_until( Some(sleep_until(self.now() + Duration::from_millis(delay_time)))
self.0.date_service.now() + Duration::from_millis(delay_time),
))
} else { } else {
None None
} }
@ -138,7 +136,7 @@ impl ServiceConfig {
pub fn client_timer_expire(&self) -> Option<Instant> { pub fn client_timer_expire(&self) -> Option<Instant> {
let delay = self.0.client_timeout; let delay = self.0.client_timeout;
if delay != 0 { if delay != 0 {
Some(self.0.date_service.now() + Duration::from_millis(delay)) Some(self.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -148,7 +146,7 @@ impl ServiceConfig {
pub fn client_disconnect_timer(&self) -> Option<Instant> { pub fn client_disconnect_timer(&self) -> Option<Instant> {
let delay = self.0.client_disconnect; let delay = self.0.client_disconnect;
if delay != 0 { if delay != 0 {
Some(self.0.date_service.now() + Duration::from_millis(delay)) Some(self.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -157,20 +155,12 @@ impl ServiceConfig {
#[inline] #[inline]
/// Return keep-alive timer delay is configured. /// Return keep-alive timer delay is configured.
pub fn keep_alive_timer(&self) -> Option<Sleep> { pub fn keep_alive_timer(&self) -> Option<Sleep> {
if let Some(ka) = self.0.keep_alive { self.keep_alive().map(|ka| sleep_until(self.now() + ka))
Some(sleep_until(self.0.date_service.now() + ka))
} else {
None
}
} }
/// Keep-alive expire time /// Keep-alive expire time
pub fn keep_alive_expire(&self) -> Option<Instant> { pub fn keep_alive_expire(&self) -> Option<Instant> {
if let Some(ka) = self.0.keep_alive { self.keep_alive().map(|ka| self.now() + ka)
Some(self.0.date_service.now() + ka)
} else {
None
}
} }
#[inline] #[inline]

View File

@ -92,7 +92,7 @@ impl<B: MessageBody> Encoder<B> {
enum EncoderBody<B> { enum EncoderBody<B> {
Bytes(Bytes), Bytes(Bytes),
Stream(#[pin] B), Stream(#[pin] B),
BoxedStream(Box<dyn MessageBody + Unpin>), BoxedStream(Pin<Box<dyn MessageBody>>),
} }
impl<B: MessageBody> MessageBody for EncoderBody<B> { impl<B: MessageBody> MessageBody for EncoderBody<B> {
@ -117,9 +117,7 @@ impl<B: MessageBody> MessageBody for EncoderBody<B> {
} }
} }
EncoderBodyProj::Stream(b) => b.poll_next(cx), EncoderBodyProj::Stream(b) => b.poll_next(cx),
EncoderBodyProj::BoxedStream(ref mut b) => { EncoderBodyProj::BoxedStream(ref mut b) => b.as_mut().poll_next(cx),
Pin::new(b.as_mut()).poll_next(cx)
}
} }
} }
} }

View File

@ -1,28 +1,20 @@
//! Error and Result module //! Error and Result module
use std::cell::RefCell; use std::{
use std::io::Write; cell::RefCell,
use std::str::Utf8Error; fmt,
use std::string::FromUtf8Error; io::{self, Write as _},
use std::{fmt, io, result}; str::Utf8Error,
string::FromUtf8Error,
};
use actix_codec::{Decoder, Encoder};
use actix_utils::dispatcher::DispatcherError as FramedDispatcherError;
use actix_utils::timeout::TimeoutError;
use bytes::BytesMut; use bytes::BytesMut;
use derive_more::{Display, From}; use derive_more::{Display, Error, From};
use http::uri::InvalidUri; use http::uri::InvalidUri;
use http::{header, Error as HttpError, StatusCode}; use http::{header, Error as HttpError, StatusCode};
use serde::de::value::Error as DeError; use serde::de::value::Error as DeError;
use serde_json::error::Error as JsonError;
use serde_urlencoded::ser::Error as FormError;
use crate::body::Body; use crate::{body::Body, helpers::Writer, Response, ResponseBuilder};
use crate::helpers::Writer;
use crate::response::{Response, ResponseBuilder};
#[cfg(feature = "cookies")]
pub use crate::cookie::ParseError as CookieParseError;
/// A specialized [`std::result::Result`] /// A specialized [`std::result::Result`]
/// for actix web operations /// for actix web operations
@ -30,7 +22,7 @@ pub use crate::cookie::ParseError as CookieParseError;
/// This typedef is generally used to avoid writing out /// This typedef is generally used to avoid writing out
/// `actix_http::error::Error` directly and is otherwise a direct mapping to /// `actix_http::error::Error` directly and is otherwise a direct mapping to
/// `Result`. /// `Result`.
pub type Result<T, E = Error> = result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
/// General purpose actix web error. /// General purpose actix web error.
/// ///
@ -54,7 +46,7 @@ impl Error {
/// Similar to `as_response_error` but downcasts. /// Similar to `as_response_error` but downcasts.
pub fn as_error<T: ResponseError + 'static>(&self) -> Option<&T> { pub fn as_error<T: ResponseError + 'static>(&self) -> Option<&T> {
ResponseError::downcast_ref(self.cause.as_ref()) <dyn ResponseError>::downcast_ref(self.cause.as_ref())
} }
} }
@ -70,7 +62,7 @@ pub trait ResponseError: fmt::Debug + fmt::Display {
/// Create response for error /// Create response for error
/// ///
/// Internal server error is generated by default. /// Internal server error is generated by default.
fn error_response(&self) -> Response { fn error_response(&self) -> Response<Body> {
let mut resp = Response::new(self.status_code()); let mut resp = Response::new(self.status_code());
let mut buf = BytesMut::new(); let mut buf = BytesMut::new();
let _ = write!(Writer(&mut buf), "{}", self); let _ = write!(Writer(&mut buf), "{}", self);
@ -119,7 +111,7 @@ impl From<std::convert::Infallible> for Error {
} }
/// Convert `Error` to a `Response` instance /// Convert `Error` to a `Response` instance
impl From<Error> for Response { impl From<Error> for Response<Body> {
fn from(err: Error) -> Self { fn from(err: Error) -> Self {
Response::from_error(err) Response::from_error(err)
} }
@ -135,8 +127,8 @@ impl<T: ResponseError + 'static> From<T> for Error {
} }
/// Convert Response to a Error /// Convert Response to a Error
impl From<Response> for Error { impl From<Response<Body>> for Error {
fn from(res: Response) -> Error { fn from(res: Response<Body>) -> Error {
InternalError::from_response("", res).into() InternalError::from_response("", res).into()
} }
} }
@ -148,34 +140,15 @@ impl From<ResponseBuilder> for Error {
} }
} }
/// Inspects the underlying enum and returns an appropriate status code. #[derive(Debug, Display, Error)]
/// #[display(fmt = "Unknown Error")]
/// If the variant is [`TimeoutError::Service`], the error code of the service is returned.
/// Otherwise, [`StatusCode::GATEWAY_TIMEOUT`] is returned.
impl<E: ResponseError> ResponseError for TimeoutError<E> {
fn status_code(&self) -> StatusCode {
match self {
TimeoutError::Service(e) => e.status_code(),
TimeoutError::Timeout => StatusCode::GATEWAY_TIMEOUT,
}
}
}
#[derive(Debug, Display)]
#[display(fmt = "UnknownError")]
struct UnitError; struct UnitError;
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`UnitError`]. /// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`UnitError`].
impl ResponseError for UnitError {} impl ResponseError for UnitError {}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`JsonError`].
impl ResponseError for JsonError {}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`FormError`].
impl ResponseError for FormError {}
#[cfg(feature = "openssl")]
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix_tls::accept::openssl::SslError`]. /// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix_tls::accept::openssl::SslError`].
#[cfg(feature = "openssl")]
impl ResponseError for actix_tls::accept::openssl::SslError {} impl ResponseError for actix_tls::accept::openssl::SslError {}
/// Returns [`StatusCode::BAD_REQUEST`] for [`DeError`]. /// Returns [`StatusCode::BAD_REQUEST`] for [`DeError`].
@ -217,38 +190,47 @@ impl ResponseError for header::InvalidHeaderValue {
} }
} }
/// A set of errors that can occur during parsing HTTP streams /// A set of errors that can occur during parsing HTTP streams.
#[derive(Debug, Display)] #[derive(Debug, Display, Error)]
#[non_exhaustive]
pub enum ParseError { pub enum ParseError {
/// An invalid `Method`, such as `GE.T`. /// An invalid `Method`, such as `GE.T`.
#[display(fmt = "Invalid Method specified")] #[display(fmt = "Invalid Method specified")]
Method, Method,
/// An invalid `Uri`, such as `exam ple.domain`. /// An invalid `Uri`, such as `exam ple.domain`.
#[display(fmt = "Uri error: {}", _0)] #[display(fmt = "Uri error: {}", _0)]
Uri(InvalidUri), Uri(InvalidUri),
/// An invalid `HttpVersion`, such as `HTP/1.1` /// An invalid `HttpVersion`, such as `HTP/1.1`
#[display(fmt = "Invalid HTTP version specified")] #[display(fmt = "Invalid HTTP version specified")]
Version, Version,
/// An invalid `Header`. /// An invalid `Header`.
#[display(fmt = "Invalid Header provided")] #[display(fmt = "Invalid Header provided")]
Header, Header,
/// A message head is too large to be reasonable. /// A message head is too large to be reasonable.
#[display(fmt = "Message head is too large")] #[display(fmt = "Message head is too large")]
TooLarge, TooLarge,
/// A message reached EOF, but is not complete. /// A message reached EOF, but is not complete.
#[display(fmt = "Message is incomplete")] #[display(fmt = "Message is incomplete")]
Incomplete, Incomplete,
/// An invalid `Status`, such as `1337 ELITE`. /// An invalid `Status`, such as `1337 ELITE`.
#[display(fmt = "Invalid Status provided")] #[display(fmt = "Invalid Status provided")]
Status, Status,
/// A timeout occurred waiting for an IO event. /// A timeout occurred waiting for an IO event.
#[allow(dead_code)] #[allow(dead_code)]
#[display(fmt = "Timeout")] #[display(fmt = "Timeout")]
Timeout, Timeout,
/// An `io::Error` that occurred while trying to read or write to a network
/// stream. /// An `io::Error` that occurred while trying to read or write to a network stream.
#[display(fmt = "IO error: {}", _0)] #[display(fmt = "IO error: {}", _0)]
Io(io::Error), Io(io::Error),
/// Parsing a field as string failed /// Parsing a field as string failed
#[display(fmt = "UTF8 error: {}", _0)] #[display(fmt = "UTF8 error: {}", _0)]
Utf8(Utf8Error), Utf8(Utf8Error),
@ -300,17 +282,16 @@ impl From<httparse::Error> for ParseError {
} }
/// A set of errors that can occur running blocking tasks in thread pool. /// A set of errors that can occur running blocking tasks in thread pool.
#[derive(Debug, Display)] #[derive(Debug, Display, Error)]
#[display(fmt = "Blocking thread pool is gone")] #[display(fmt = "Blocking thread pool is gone")]
pub struct BlockingError; pub struct BlockingError;
impl std::error::Error for BlockingError {}
/// `InternalServerError` for `BlockingError` /// `InternalServerError` for `BlockingError`
impl ResponseError for BlockingError {} impl ResponseError for BlockingError {}
#[derive(Display, Debug)] /// A set of errors that can occur during payload parsing.
/// A set of errors that can occur during payload parsing #[derive(Debug, Display)]
#[non_exhaustive]
pub enum PayloadError { pub enum PayloadError {
/// A payload reached EOF, but is not complete. /// A payload reached EOF, but is not complete.
#[display( #[display(
@ -394,16 +375,9 @@ impl ResponseError for PayloadError {
} }
} }
/// Return `BadRequest` for `cookie::ParseError` /// A set of errors that can occur during dispatching HTTP requests.
#[cfg(feature = "cookies")] #[derive(Debug, Display, Error, From)]
impl ResponseError for crate::cookie::ParseError { #[non_exhaustive]
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
#[derive(Debug, Display, From)]
/// A set of errors that can occur during dispatching HTTP requests
pub enum DispatchError { pub enum DispatchError {
/// Service error /// Service error
Service(Error), Service(Error),
@ -449,18 +423,34 @@ pub enum DispatchError {
Unknown, Unknown,
} }
/// A set of error that can occur during parsing content type /// A set of error that can occur during parsing content type.
#[derive(PartialEq, Debug, Display)] #[derive(Debug, Display, Error)]
#[non_exhaustive]
pub enum ContentTypeError { pub enum ContentTypeError {
/// Can not parse content type /// Can not parse content type
#[display(fmt = "Can not parse content type")] #[display(fmt = "Can not parse content type")]
ParseError, ParseError,
/// Unknown content encoding /// Unknown content encoding
#[display(fmt = "Unknown content encoding")] #[display(fmt = "Unknown content encoding")]
UnknownEncoding, UnknownEncoding,
} }
impl std::error::Error for ContentTypeError {} #[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)
}
}
}
}
}
/// Return `BadRequest` for `ContentTypeError` /// Return `BadRequest` for `ContentTypeError`
impl ResponseError for ContentTypeError { impl ResponseError for ContentTypeError {
@ -469,21 +459,13 @@ impl ResponseError for ContentTypeError {
} }
} }
impl<E, U: Encoder<I> + Decoder, I> ResponseError for FramedDispatcherError<E, U, I>
where
E: fmt::Debug + fmt::Display,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
}
/// Helper type that can wrap any error and generate custom response. /// Helper type that can wrap any error and generate custom response.
/// ///
/// In following example any `io::Error` will be converted into "BAD REQUEST" /// In following example any `io::Error` will be converted into "BAD REQUEST"
/// response as opposite to *INTERNAL SERVER ERROR* which is defined by /// response as opposite to *INTERNAL SERVER ERROR* which is defined by
/// default. /// default.
/// ///
/// ```rust /// ```
/// # use std::io; /// # use std::io;
/// # use actix_http::*; /// # use actix_http::*;
/// ///
@ -498,7 +480,7 @@ pub struct InternalError<T> {
enum InternalErrorType { enum InternalErrorType {
Status(StatusCode), Status(StatusCode),
Response(RefCell<Option<Response>>), Response(RefCell<Option<Response<Body>>>),
} }
impl<T> InternalError<T> { impl<T> InternalError<T> {
@ -511,7 +493,7 @@ impl<T> InternalError<T> {
} }
/// Create `InternalError` with predefined `Response`. /// Create `InternalError` with predefined `Response`.
pub fn from_response(cause: T, response: Response) -> Self { pub fn from_response(cause: T, response: Response<Body>) -> Self {
InternalError { InternalError {
cause, cause,
status: InternalErrorType::Response(RefCell::new(Some(response))), status: InternalErrorType::Response(RefCell::new(Some(response))),
@ -554,7 +536,7 @@ where
} }
} }
fn error_response(&self) -> Response { fn error_response(&self) -> Response<Body> {
match self.status { match self.status {
InternalErrorType::Status(st) => { InternalErrorType::Status(st) => {
let mut res = Response::new(st); let mut res = Response::new(st);
@ -577,395 +559,72 @@ where
} }
} }
/// Helper function that creates wrapper of any error and generate *BAD macro_rules! error_helper {
/// REQUEST* response. ($name:ident, $status:ident) => {
#[allow(non_snake_case)] paste::paste! {
pub fn ErrorBadRequest<T>(err: T) -> Error #[doc = "Helper function that wraps any error and generates a `" $status "` response."]
where #[allow(non_snake_case)]
T: fmt::Debug + fmt::Display + 'static, pub fn $name<T>(err: T) -> Error
{ where
InternalError::new(err, StatusCode::BAD_REQUEST).into() T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::$status).into()
}
}
}
} }
/// Helper function that creates wrapper of any error and generate error_helper!(ErrorBadRequest, BAD_REQUEST);
/// *UNAUTHORIZED* response. error_helper!(ErrorUnauthorized, UNAUTHORIZED);
#[allow(non_snake_case)] error_helper!(ErrorPaymentRequired, PAYMENT_REQUIRED);
pub fn ErrorUnauthorized<T>(err: T) -> Error error_helper!(ErrorForbidden, FORBIDDEN);
where error_helper!(ErrorNotFound, NOT_FOUND);
T: fmt::Debug + fmt::Display + 'static, error_helper!(ErrorMethodNotAllowed, METHOD_NOT_ALLOWED);
{ error_helper!(ErrorNotAcceptable, NOT_ACCEPTABLE);
InternalError::new(err, StatusCode::UNAUTHORIZED).into() error_helper!(
} ErrorProxyAuthenticationRequired,
PROXY_AUTHENTICATION_REQUIRED
/// Helper function that creates wrapper of any error and generate );
/// *PAYMENT_REQUIRED* response. error_helper!(ErrorRequestTimeout, REQUEST_TIMEOUT);
#[allow(non_snake_case)] error_helper!(ErrorConflict, CONFLICT);
pub fn ErrorPaymentRequired<T>(err: T) -> Error error_helper!(ErrorGone, GONE);
where error_helper!(ErrorLengthRequired, LENGTH_REQUIRED);
T: fmt::Debug + fmt::Display + 'static, error_helper!(ErrorPayloadTooLarge, PAYLOAD_TOO_LARGE);
{ error_helper!(ErrorUriTooLong, URI_TOO_LONG);
InternalError::new(err, StatusCode::PAYMENT_REQUIRED).into() error_helper!(ErrorUnsupportedMediaType, UNSUPPORTED_MEDIA_TYPE);
} error_helper!(ErrorRangeNotSatisfiable, RANGE_NOT_SATISFIABLE);
error_helper!(ErrorImATeapot, IM_A_TEAPOT);
/// Helper function that creates wrapper of any error and generate *FORBIDDEN* error_helper!(ErrorMisdirectedRequest, MISDIRECTED_REQUEST);
/// response. error_helper!(ErrorUnprocessableEntity, UNPROCESSABLE_ENTITY);
#[allow(non_snake_case)] error_helper!(ErrorLocked, LOCKED);
pub fn ErrorForbidden<T>(err: T) -> Error error_helper!(ErrorFailedDependency, FAILED_DEPENDENCY);
where error_helper!(ErrorUpgradeRequired, UPGRADE_REQUIRED);
T: fmt::Debug + fmt::Display + 'static, error_helper!(ErrorPreconditionFailed, PRECONDITION_FAILED);
{ error_helper!(ErrorPreconditionRequired, PRECONDITION_REQUIRED);
InternalError::new(err, StatusCode::FORBIDDEN).into() error_helper!(ErrorTooManyRequests, TOO_MANY_REQUESTS);
} error_helper!(
ErrorRequestHeaderFieldsTooLarge,
/// Helper function that creates wrapper of any error and generate *NOT FOUND* REQUEST_HEADER_FIELDS_TOO_LARGE
/// response. );
#[allow(non_snake_case)] error_helper!(
pub fn ErrorNotFound<T>(err: T) -> Error ErrorUnavailableForLegalReasons,
where UNAVAILABLE_FOR_LEGAL_REASONS
T: fmt::Debug + fmt::Display + 'static, );
{ error_helper!(ErrorExpectationFailed, EXPECTATION_FAILED);
InternalError::new(err, StatusCode::NOT_FOUND).into() error_helper!(ErrorInternalServerError, INTERNAL_SERVER_ERROR);
} error_helper!(ErrorNotImplemented, NOT_IMPLEMENTED);
error_helper!(ErrorBadGateway, BAD_GATEWAY);
/// Helper function that creates wrapper of any error and generate *METHOD NOT error_helper!(ErrorServiceUnavailable, SERVICE_UNAVAILABLE);
/// ALLOWED* response. error_helper!(ErrorGatewayTimeout, GATEWAY_TIMEOUT);
#[allow(non_snake_case)] error_helper!(ErrorHttpVersionNotSupported, HTTP_VERSION_NOT_SUPPORTED);
pub fn ErrorMethodNotAllowed<T>(err: T) -> Error error_helper!(ErrorVariantAlsoNegotiates, VARIANT_ALSO_NEGOTIATES);
where error_helper!(ErrorInsufficientStorage, INSUFFICIENT_STORAGE);
T: fmt::Debug + fmt::Display + 'static, error_helper!(ErrorLoopDetected, LOOP_DETECTED);
{ error_helper!(ErrorNotExtended, NOT_EXTENDED);
InternalError::new(err, StatusCode::METHOD_NOT_ALLOWED).into() error_helper!(
} ErrorNetworkAuthenticationRequired,
NETWORK_AUTHENTICATION_REQUIRED
/// Helper function that creates wrapper of any error and generate *NOT );
/// ACCEPTABLE* response.
#[allow(non_snake_case)]
pub fn ErrorNotAcceptable<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::NOT_ACCEPTABLE).into()
}
/// Helper function that creates wrapper of any error and generate *PROXY
/// AUTHENTICATION REQUIRED* response.
#[allow(non_snake_case)]
pub fn ErrorProxyAuthenticationRequired<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::PROXY_AUTHENTICATION_REQUIRED).into()
}
/// Helper function that creates wrapper of any error and generate *REQUEST
/// TIMEOUT* response.
#[allow(non_snake_case)]
pub fn ErrorRequestTimeout<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::REQUEST_TIMEOUT).into()
}
/// Helper function that creates wrapper of any error and generate *CONFLICT*
/// response.
#[allow(non_snake_case)]
pub fn ErrorConflict<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::CONFLICT).into()
}
/// Helper function that creates wrapper of any error and generate *GONE*
/// response.
#[allow(non_snake_case)]
pub fn ErrorGone<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::GONE).into()
}
/// Helper function that creates wrapper of any error and generate *LENGTH
/// REQUIRED* response.
#[allow(non_snake_case)]
pub fn ErrorLengthRequired<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::LENGTH_REQUIRED).into()
}
/// Helper function that creates wrapper of any error and generate
/// *PAYLOAD TOO LARGE* response.
#[allow(non_snake_case)]
pub fn ErrorPayloadTooLarge<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::PAYLOAD_TOO_LARGE).into()
}
/// Helper function that creates wrapper of any error and generate
/// *URI TOO LONG* response.
#[allow(non_snake_case)]
pub fn ErrorUriTooLong<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::URI_TOO_LONG).into()
}
/// Helper function that creates wrapper of any error and generate
/// *UNSUPPORTED MEDIA TYPE* response.
#[allow(non_snake_case)]
pub fn ErrorUnsupportedMediaType<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::UNSUPPORTED_MEDIA_TYPE).into()
}
/// Helper function that creates wrapper of any error and generate
/// *RANGE NOT SATISFIABLE* response.
#[allow(non_snake_case)]
pub fn ErrorRangeNotSatisfiable<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::RANGE_NOT_SATISFIABLE).into()
}
/// Helper function that creates wrapper of any error and generate
/// *IM A TEAPOT* response.
#[allow(non_snake_case)]
pub fn ErrorImATeapot<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::IM_A_TEAPOT).into()
}
/// Helper function that creates wrapper of any error and generate
/// *MISDIRECTED REQUEST* response.
#[allow(non_snake_case)]
pub fn ErrorMisdirectedRequest<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::MISDIRECTED_REQUEST).into()
}
/// Helper function that creates wrapper of any error and generate
/// *UNPROCESSABLE ENTITY* response.
#[allow(non_snake_case)]
pub fn ErrorUnprocessableEntity<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::UNPROCESSABLE_ENTITY).into()
}
/// Helper function that creates wrapper of any error and generate
/// *LOCKED* response.
#[allow(non_snake_case)]
pub fn ErrorLocked<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::LOCKED).into()
}
/// Helper function that creates wrapper of any error and generate
/// *FAILED DEPENDENCY* response.
#[allow(non_snake_case)]
pub fn ErrorFailedDependency<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::FAILED_DEPENDENCY).into()
}
/// Helper function that creates wrapper of any error and generate
/// *UPGRADE REQUIRED* response.
#[allow(non_snake_case)]
pub fn ErrorUpgradeRequired<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::UPGRADE_REQUIRED).into()
}
/// Helper function that creates wrapper of any error and generate
/// *PRECONDITION FAILED* response.
#[allow(non_snake_case)]
pub fn ErrorPreconditionFailed<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::PRECONDITION_FAILED).into()
}
/// Helper function that creates wrapper of any error and generate
/// *PRECONDITION REQUIRED* response.
#[allow(non_snake_case)]
pub fn ErrorPreconditionRequired<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::PRECONDITION_REQUIRED).into()
}
/// Helper function that creates wrapper of any error and generate
/// *TOO MANY REQUESTS* response.
#[allow(non_snake_case)]
pub fn ErrorTooManyRequests<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::TOO_MANY_REQUESTS).into()
}
/// Helper function that creates wrapper of any error and generate
/// *REQUEST HEADER FIELDS TOO LARGE* response.
#[allow(non_snake_case)]
pub fn ErrorRequestHeaderFieldsTooLarge<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE).into()
}
/// Helper function that creates wrapper of any error and generate
/// *UNAVAILABLE FOR LEGAL REASONS* response.
#[allow(non_snake_case)]
pub fn ErrorUnavailableForLegalReasons<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS).into()
}
/// Helper function that creates wrapper of any error and generate
/// *EXPECTATION FAILED* response.
#[allow(non_snake_case)]
pub fn ErrorExpectationFailed<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::EXPECTATION_FAILED).into()
}
/// Helper function that creates wrapper of any error and
/// generate *INTERNAL SERVER ERROR* response.
#[allow(non_snake_case)]
pub fn ErrorInternalServerError<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR).into()
}
/// Helper function that creates wrapper of any error and
/// generate *NOT IMPLEMENTED* response.
#[allow(non_snake_case)]
pub fn ErrorNotImplemented<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::NOT_IMPLEMENTED).into()
}
/// Helper function that creates wrapper of any error and
/// generate *BAD GATEWAY* response.
#[allow(non_snake_case)]
pub fn ErrorBadGateway<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::BAD_GATEWAY).into()
}
/// Helper function that creates wrapper of any error and
/// generate *SERVICE UNAVAILABLE* response.
#[allow(non_snake_case)]
pub fn ErrorServiceUnavailable<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::SERVICE_UNAVAILABLE).into()
}
/// Helper function that creates wrapper of any error and
/// generate *GATEWAY TIMEOUT* response.
#[allow(non_snake_case)]
pub fn ErrorGatewayTimeout<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::GATEWAY_TIMEOUT).into()
}
/// Helper function that creates wrapper of any error and
/// generate *HTTP VERSION NOT SUPPORTED* response.
#[allow(non_snake_case)]
pub fn ErrorHttpVersionNotSupported<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::HTTP_VERSION_NOT_SUPPORTED).into()
}
/// Helper function that creates wrapper of any error and
/// generate *VARIANT ALSO NEGOTIATES* response.
#[allow(non_snake_case)]
pub fn ErrorVariantAlsoNegotiates<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::VARIANT_ALSO_NEGOTIATES).into()
}
/// Helper function that creates wrapper of any error and
/// generate *INSUFFICIENT STORAGE* response.
#[allow(non_snake_case)]
pub fn ErrorInsufficientStorage<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::INSUFFICIENT_STORAGE).into()
}
/// Helper function that creates wrapper of any error and
/// generate *LOOP DETECTED* response.
#[allow(non_snake_case)]
pub fn ErrorLoopDetected<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::LOOP_DETECTED).into()
}
/// Helper function that creates wrapper of any error and
/// generate *NOT EXTENDED* response.
#[allow(non_snake_case)]
pub fn ErrorNotExtended<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::NOT_EXTENDED).into()
}
/// Helper function that creates wrapper of any error and
/// generate *NETWORK AUTHENTICATION REQUIRED* response.
#[allow(non_snake_case)]
pub fn ErrorNetworkAuthenticationRequired<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -975,21 +634,14 @@ mod tests {
#[test] #[test]
fn test_into_response() { fn test_into_response() {
let resp: Response = ParseError::Incomplete.error_response(); let resp: Response<Body> = ParseError::Incomplete.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into();
let resp: Response = err.error_response(); let resp: Response<Body> = err.error_response();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }
#[cfg(feature = "cookies")]
#[test]
fn test_cookie_parse() {
let resp: Response = CookieParseError::EmptyName.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test] #[test]
fn test_as_response() { fn test_as_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::new(io::ErrorKind::Other, "other");
@ -1017,7 +669,7 @@ mod tests {
fn test_error_http_response() { fn test_error_http_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::new(io::ErrorKind::Other, "other");
let e = Error::from(orig); let e = Error::from(orig);
let resp: Response = e.into(); let resp: Response<Body> = e.into();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }
@ -1072,9 +724,8 @@ mod tests {
#[test] #[test]
fn test_internal_error() { fn test_internal_error() {
let err = let err = InternalError::from_response(ParseError::Method, Response::ok());
InternalError::from_response(ParseError::Method, Response::Ok().into()); let resp: Response<Body> = err.error_response();
let resp: Response = err.error_response();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
@ -1090,121 +741,121 @@ mod tests {
#[test] #[test]
fn test_error_helpers() { fn test_error_helpers() {
let r: Response = ErrorBadRequest("err").into(); let res: Response<Body> = ErrorBadRequest("err").into();
assert_eq!(r.status(), StatusCode::BAD_REQUEST); assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let r: Response = ErrorUnauthorized("err").into(); let res: Response<Body> = ErrorUnauthorized("err").into();
assert_eq!(r.status(), StatusCode::UNAUTHORIZED); assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
let r: Response = ErrorPaymentRequired("err").into(); let res: Response<Body> = ErrorPaymentRequired("err").into();
assert_eq!(r.status(), StatusCode::PAYMENT_REQUIRED); assert_eq!(res.status(), StatusCode::PAYMENT_REQUIRED);
let r: Response = ErrorForbidden("err").into(); let res: Response<Body> = ErrorForbidden("err").into();
assert_eq!(r.status(), StatusCode::FORBIDDEN); assert_eq!(res.status(), StatusCode::FORBIDDEN);
let r: Response = ErrorNotFound("err").into(); let res: Response<Body> = ErrorNotFound("err").into();
assert_eq!(r.status(), StatusCode::NOT_FOUND); assert_eq!(res.status(), StatusCode::NOT_FOUND);
let r: Response = ErrorMethodNotAllowed("err").into(); let res: Response<Body> = ErrorMethodNotAllowed("err").into();
assert_eq!(r.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
let r: Response = ErrorNotAcceptable("err").into(); let res: Response<Body> = ErrorNotAcceptable("err").into();
assert_eq!(r.status(), StatusCode::NOT_ACCEPTABLE); assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE);
let r: Response = ErrorProxyAuthenticationRequired("err").into(); let res: Response<Body> = ErrorProxyAuthenticationRequired("err").into();
assert_eq!(r.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED); assert_eq!(res.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED);
let r: Response = ErrorRequestTimeout("err").into(); let res: Response<Body> = ErrorRequestTimeout("err").into();
assert_eq!(r.status(), StatusCode::REQUEST_TIMEOUT); assert_eq!(res.status(), StatusCode::REQUEST_TIMEOUT);
let r: Response = ErrorConflict("err").into(); let res: Response<Body> = ErrorConflict("err").into();
assert_eq!(r.status(), StatusCode::CONFLICT); assert_eq!(res.status(), StatusCode::CONFLICT);
let r: Response = ErrorGone("err").into(); let res: Response<Body> = ErrorGone("err").into();
assert_eq!(r.status(), StatusCode::GONE); assert_eq!(res.status(), StatusCode::GONE);
let r: Response = ErrorLengthRequired("err").into(); let res: Response<Body> = ErrorLengthRequired("err").into();
assert_eq!(r.status(), StatusCode::LENGTH_REQUIRED); assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED);
let r: Response = ErrorPreconditionFailed("err").into(); let res: Response<Body> = ErrorPreconditionFailed("err").into();
assert_eq!(r.status(), StatusCode::PRECONDITION_FAILED); assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
let r: Response = ErrorPayloadTooLarge("err").into(); let res: Response<Body> = ErrorPayloadTooLarge("err").into();
assert_eq!(r.status(), StatusCode::PAYLOAD_TOO_LARGE); assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
let r: Response = ErrorUriTooLong("err").into(); let res: Response<Body> = ErrorUriTooLong("err").into();
assert_eq!(r.status(), StatusCode::URI_TOO_LONG); assert_eq!(res.status(), StatusCode::URI_TOO_LONG);
let r: Response = ErrorUnsupportedMediaType("err").into(); let res: Response<Body> = ErrorUnsupportedMediaType("err").into();
assert_eq!(r.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
let r: Response = ErrorRangeNotSatisfiable("err").into(); let res: Response<Body> = ErrorRangeNotSatisfiable("err").into();
assert_eq!(r.status(), StatusCode::RANGE_NOT_SATISFIABLE); assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE);
let r: Response = ErrorExpectationFailed("err").into(); let res: Response<Body> = ErrorExpectationFailed("err").into();
assert_eq!(r.status(), StatusCode::EXPECTATION_FAILED); assert_eq!(res.status(), StatusCode::EXPECTATION_FAILED);
let r: Response = ErrorImATeapot("err").into(); let res: Response<Body> = ErrorImATeapot("err").into();
assert_eq!(r.status(), StatusCode::IM_A_TEAPOT); assert_eq!(res.status(), StatusCode::IM_A_TEAPOT);
let r: Response = ErrorMisdirectedRequest("err").into(); let res: Response<Body> = ErrorMisdirectedRequest("err").into();
assert_eq!(r.status(), StatusCode::MISDIRECTED_REQUEST); assert_eq!(res.status(), StatusCode::MISDIRECTED_REQUEST);
let r: Response = ErrorUnprocessableEntity("err").into(); let res: Response<Body> = ErrorUnprocessableEntity("err").into();
assert_eq!(r.status(), StatusCode::UNPROCESSABLE_ENTITY); assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let r: Response = ErrorLocked("err").into(); let res: Response<Body> = ErrorLocked("err").into();
assert_eq!(r.status(), StatusCode::LOCKED); assert_eq!(res.status(), StatusCode::LOCKED);
let r: Response = ErrorFailedDependency("err").into(); let res: Response<Body> = ErrorFailedDependency("err").into();
assert_eq!(r.status(), StatusCode::FAILED_DEPENDENCY); assert_eq!(res.status(), StatusCode::FAILED_DEPENDENCY);
let r: Response = ErrorUpgradeRequired("err").into(); let res: Response<Body> = ErrorUpgradeRequired("err").into();
assert_eq!(r.status(), StatusCode::UPGRADE_REQUIRED); assert_eq!(res.status(), StatusCode::UPGRADE_REQUIRED);
let r: Response = ErrorPreconditionRequired("err").into(); let res: Response<Body> = ErrorPreconditionRequired("err").into();
assert_eq!(r.status(), StatusCode::PRECONDITION_REQUIRED); assert_eq!(res.status(), StatusCode::PRECONDITION_REQUIRED);
let r: Response = ErrorTooManyRequests("err").into(); let res: Response<Body> = ErrorTooManyRequests("err").into();
assert_eq!(r.status(), StatusCode::TOO_MANY_REQUESTS); assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS);
let r: Response = ErrorRequestHeaderFieldsTooLarge("err").into(); let res: Response<Body> = ErrorRequestHeaderFieldsTooLarge("err").into();
assert_eq!(r.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE); assert_eq!(res.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
let r: Response = ErrorUnavailableForLegalReasons("err").into(); let res: Response<Body> = ErrorUnavailableForLegalReasons("err").into();
assert_eq!(r.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); assert_eq!(res.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);
let r: Response = ErrorInternalServerError("err").into(); let res: Response<Body> = ErrorInternalServerError("err").into();
assert_eq!(r.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
let r: Response = ErrorNotImplemented("err").into(); let res: Response<Body> = ErrorNotImplemented("err").into();
assert_eq!(r.status(), StatusCode::NOT_IMPLEMENTED); assert_eq!(res.status(), StatusCode::NOT_IMPLEMENTED);
let r: Response = ErrorBadGateway("err").into(); let res: Response<Body> = ErrorBadGateway("err").into();
assert_eq!(r.status(), StatusCode::BAD_GATEWAY); assert_eq!(res.status(), StatusCode::BAD_GATEWAY);
let r: Response = ErrorServiceUnavailable("err").into(); let res: Response<Body> = ErrorServiceUnavailable("err").into();
assert_eq!(r.status(), StatusCode::SERVICE_UNAVAILABLE); assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE);
let r: Response = ErrorGatewayTimeout("err").into(); let res: Response<Body> = ErrorGatewayTimeout("err").into();
assert_eq!(r.status(), StatusCode::GATEWAY_TIMEOUT); assert_eq!(res.status(), StatusCode::GATEWAY_TIMEOUT);
let r: Response = ErrorHttpVersionNotSupported("err").into(); let res: Response<Body> = ErrorHttpVersionNotSupported("err").into();
assert_eq!(r.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED); assert_eq!(res.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
let r: Response = ErrorVariantAlsoNegotiates("err").into(); let res: Response<Body> = ErrorVariantAlsoNegotiates("err").into();
assert_eq!(r.status(), StatusCode::VARIANT_ALSO_NEGOTIATES); assert_eq!(res.status(), StatusCode::VARIANT_ALSO_NEGOTIATES);
let r: Response = ErrorInsufficientStorage("err").into(); let res: Response<Body> = ErrorInsufficientStorage("err").into();
assert_eq!(r.status(), StatusCode::INSUFFICIENT_STORAGE); assert_eq!(res.status(), StatusCode::INSUFFICIENT_STORAGE);
let r: Response = ErrorLoopDetected("err").into(); let res: Response<Body> = ErrorLoopDetected("err").into();
assert_eq!(r.status(), StatusCode::LOOP_DETECTED); assert_eq!(res.status(), StatusCode::LOOP_DETECTED);
let r: Response = ErrorNotExtended("err").into(); let res: Response<Body> = ErrorNotExtended("err").into();
assert_eq!(r.status(), StatusCode::NOT_EXTENDED); assert_eq!(res.status(), StatusCode::NOT_EXTENDED);
let r: Response = ErrorNetworkAuthenticationRequired("err").into(); let res: Response<Body> = ErrorNetworkAuthenticationRequired("err").into();
assert_eq!(r.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED); assert_eq!(res.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED);
} }
} }

View File

@ -1213,8 +1213,9 @@ mod tests {
#[test] #[test]
fn test_parse_chunked_payload_chunk_extension() { fn test_parse_chunked_payload_chunk_extension() {
let mut buf = BytesMut::from( let mut buf = BytesMut::from(
&"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n"[..], transfer-encoding: chunked\r\n\
\r\n",
); );
let mut reader = MessageDecoder::<Request>::default(); let mut reader = MessageDecoder::<Request>::default();
@ -1233,7 +1234,7 @@ mod tests {
#[test] #[test]
fn test_response_http10_read_until_eof() { fn test_response_http10_read_until_eof() {
let mut buf = BytesMut::from(&"HTTP/1.0 200 Ok\r\n\r\ntest data"[..]); let mut buf = BytesMut::from("HTTP/1.0 200 Ok\r\n\r\ntest data");
let mut reader = MessageDecoder::<ResponseHead>::default(); let mut reader = MessageDecoder::<ResponseHead>::default();
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();

View File

@ -21,6 +21,7 @@ use crate::body::{Body, BodySize, MessageBody, ResponseBody};
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error}; use crate::error::{DispatchError, Error};
use crate::error::{ParseError, PayloadError}; use crate::error::{ParseError, PayloadError};
use crate::http::StatusCode;
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::service::HttpFlow; use crate::service::HttpFlow;
@ -346,7 +347,7 @@ where
// send service call error as response // send service call error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response = err.into().into(); let res = Response::from_error(err.into());
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_response(res, body.into_body())?; self.as_mut().send_response(res, body.into_body())?;
} }
@ -407,7 +408,7 @@ where
} }
// send expect error as response // send expect error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response = err.into().into(); let res = Response::from_error(err.into());
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_response(res, body.into_body())?; self.as_mut().send_response(res, body.into_body())?;
} }
@ -456,8 +457,7 @@ where
// to notify the dispatcher a new state is set and the outer loop // to notify the dispatcher a new state is set and the outer loop
// should be continue. // should be continue.
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let err = err.into(); let res = Response::from_error(err.into());
let res: Response = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
return self.send_response(res, body.into_body()); return self.send_response(res, body.into_body());
} }
@ -477,7 +477,7 @@ where
Poll::Pending => Ok(()), Poll::Pending => Ok(()),
// see the comment on ExpectCall state branch's Ready(Err(err)). // see the comment on ExpectCall state branch's Ready(Err(err)).
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response = err.into().into(); let res = Response::from_error(err.into());
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.send_response(res, body.into_body()) self.send_response(res, body.into_body())
} }
@ -563,7 +563,7 @@ where
); );
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::InternalServerError().finish().drop_body(), Response::internal_server_error().drop_body(),
)); ));
*this.error = Some(DispatchError::InternalError); *this.error = Some(DispatchError::InternalError);
break; break;
@ -576,7 +576,7 @@ where
error!("Internal server error: unexpected eof"); error!("Internal server error: unexpected eof");
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::InternalServerError().finish().drop_body(), Response::internal_server_error().drop_body(),
)); ));
*this.error = Some(DispatchError::InternalError); *this.error = Some(DispatchError::InternalError);
break; break;
@ -599,7 +599,8 @@ where
} }
// Requests overflow buffer size should be responded with 431 // Requests overflow buffer size should be responded with 431
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::RequestHeaderFieldsTooLarge().finish().drop_body(), Response::new(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE)
.drop_body(),
)); ));
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
*this.error = Some(ParseError::TooLarge.into()); *this.error = Some(ParseError::TooLarge.into());
@ -612,7 +613,7 @@ where
// Malformed requests should be responded with 400 // Malformed requests should be responded with 400
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::BadRequest().finish().drop_body(), Response::bad_request().drop_body(),
)); ));
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
*this.error = Some(err.into()); *this.error = Some(err.into());
@ -648,11 +649,6 @@ where
// go into Some<Pin<&mut Sleep>> branch // go into Some<Pin<&mut Sleep>> branch
this.ka_timer.set(Some(sleep_until(deadline))); this.ka_timer.set(Some(sleep_until(deadline)));
return self.poll_keepalive(cx); return self.poll_keepalive(cx);
} else {
this.flags.insert(Flags::READ_DISCONNECT);
if let Some(mut payload) = this.payload.take() {
payload.set_error(PayloadError::Incomplete(None));
}
} }
} }
} }
@ -662,7 +658,7 @@ where
// got timeout during shutdown, drop connection // got timeout during shutdown, drop connection
if this.flags.contains(Flags::SHUTDOWN) { if this.flags.contains(Flags::SHUTDOWN) {
return Err(DispatchError::DisconnectTimeout); return Err(DispatchError::DisconnectTimeout);
// exceed deadline. check for any outstanding tasks // exceed deadline. check for any outstanding tasks
} else if timer.deadline() >= *this.ka_expire { } else if timer.deadline() >= *this.ka_expire {
// have no task at hand. // have no task at hand.
if this.state.is_empty() && this.write_buf.is_empty() { if this.state.is_empty() && this.write_buf.is_empty() {
@ -682,28 +678,24 @@ where
} }
} else { } else {
// timeout on first request (slow request) return 408 // timeout on first request (slow request) return 408
if !this.flags.contains(Flags::STARTED) { trace!("Slow request timeout");
trace!("Slow request timeout"); let _ = self.as_mut().send_response(
let _ = self.as_mut().send_response( Response::new(StatusCode::REQUEST_TIMEOUT)
Response::RequestTimeout().finish().drop_body(), .drop_body(),
ResponseBody::Other(Body::Empty), ResponseBody::Other(Body::Empty),
); );
this = self.project(); this = self.project();
} else {
trace!("Keep-alive connection timeout");
}
this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN);
this.state.set(State::None);
} }
// still have unfinished task. try to reset and register keep-alive. // still have unfinished task. try to reset and register keep-alive.
} else if let Some(deadline) = } else if let Some(deadline) =
this.codec.config().keep_alive_expire() this.codec.config().keep_alive_expire()
{ {
timer.as_mut().reset(deadline); timer.as_mut().reset(deadline);
let _ = timer.poll(cx); let _ = timer.poll(cx);
} }
// timer resolved but still have not met the keep-alive expire deadline. // timer resolved but still have not met the keep-alive expire deadline.
// reset and register for later wakeup. // reset and register for later wakeup.
} else { } else {
timer.as_mut().reset(*this.ka_expire); timer.as_mut().reset(*this.ka_expire);
let _ = timer.poll(cx); let _ = timer.poll(cx);
@ -951,14 +943,17 @@ mod tests {
use std::str; use std::str;
use actix_service::fn_service; use actix_service::fn_service;
use futures_util::future::{lazy, ready}; use actix_utils::future::{ready, Ready};
use bytes::Bytes;
use futures_util::future::lazy;
use super::*; use super::*;
use crate::test::TestBuffer;
use crate::{error::Error, KeepAlive};
use crate::{ use crate::{
error::Error,
h1::{ExpectHandler, UpgradeHandler}, h1::{ExpectHandler, UpgradeHandler},
test::TestSeqBuffer, http::Method,
test::{TestBuffer, TestSeqBuffer},
HttpMessage, KeepAlive,
}; };
fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> { fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
@ -977,19 +972,22 @@ mod tests {
} }
} }
fn ok_service() -> impl Service<Request, Response = Response, Error = Error> { fn ok_service() -> impl Service<Request, Response = Response<Body>, Error = Error> {
fn_service(|_req: Request| ready(Ok::<_, Error>(Response::Ok().finish()))) fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok())))
} }
fn echo_path_service() -> impl Service<Request, Response = Response, Error = Error> { fn echo_path_service(
) -> impl Service<Request, Response = Response<Body>, Error = Error> {
fn_service(|req: Request| { fn_service(|req: Request| {
let path = req.path().as_bytes(); let path = req.path().as_bytes();
ready(Ok::<_, Error>(Response::Ok().body(Body::from_slice(path)))) ready(Ok::<_, Error>(
Response::ok().set_body(Body::from_slice(path)),
))
}) })
} }
fn echo_payload_service() -> impl Service<Request, Response = Response, Error = Error> fn echo_payload_service(
{ ) -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
fn_service(|mut req: Request| { fn_service(|mut req: Request| {
Box::pin(async move { Box::pin(async move {
use futures_util::stream::StreamExt as _; use futures_util::stream::StreamExt as _;
@ -1000,7 +998,7 @@ mod tests {
body.extend_from_slice(chunk.unwrap().chunk()) body.extend_from_slice(chunk.unwrap().chunk())
} }
Ok::<_, Error>(Response::Ok().body(body)) Ok::<_, Error>(Response::ok().set_body(body.freeze()))
}) })
}) })
} }
@ -1282,14 +1280,30 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_upgrade() { async fn test_upgrade() {
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| { lazy(|cx| {
let mut buf = TestSeqBuffer::empty(); let mut buf = TestSeqBuffer::empty();
let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None); let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None);
let services = let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade));
HttpFlow::new(ok_service(), ExpectHandler, Some(UpgradeHandler));
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( let h1 = Dispatcher::<_, _, _, _, TestUpgrade>::new(
buf.clone(), buf.clone(),
cfg, cfg,
services, services,

View File

@ -1,7 +1,5 @@
use std::task::Poll;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use futures_util::future::{ready, Ready}; use actix_utils::future::{ready, Ready};
use crate::error::Error; use crate::error::Error;
use crate::request::Request; use crate::request::Request;

View File

@ -263,7 +263,7 @@ impl Inner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use futures_util::future::poll_fn; use actix_utils::future::poll_fn;
#[actix_rt::test] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {

View File

@ -1,22 +1,22 @@
use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin;
use std::rc::Rc; use std::rc::Rc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::{fmt, net}; use std::{fmt, net};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; use actix_service::{
use futures_core::ready; fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
use futures_util::future::ready; };
use actix_utils::future::ready;
use futures_core::future::LocalBoxFuture;
use crate::body::MessageBody; use crate::body::MessageBody;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error}; use crate::error::{DispatchError, Error};
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::service::HttpFlow; use crate::service::HttpServiceHandler;
use crate::{ConnectCallback, OnConnectData}; use crate::{ConnectCallback, OnConnectData};
use super::codec::Codec; use super::codec::Codec;
@ -60,14 +60,17 @@ where
impl<S, B, X, U> H1Service<TcpStream, S, B, X, U> impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Error>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
@ -81,7 +84,7 @@ where
Error = DispatchError, Error = DispatchError,
InitError = (), InitError = (),
> { > {
pipeline_factory(|io: TcpStream| { fn_service(|io: TcpStream| {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
ready(Ok((io, peer_addr))) ready(Ok((io, peer_addr)))
}) })
@ -94,17 +97,21 @@ mod openssl {
use super::*; use super::*;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
TlsError,
};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Error>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@ -112,6 +119,7 @@ mod openssl {
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
@ -126,16 +134,14 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = (), InitError = (),
> { > {
pipeline_factory( Acceptor::new(acceptor)
Acceptor::new(acceptor) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| {
) let peer_addr = io.get_ref().peer_addr().ok();
.and_then(|io: TlsStream<TcpStream>| { ready(Ok((io, peer_addr)))
let peer_addr = io.get_ref().peer_addr().ok(); })
ready(Ok((io, peer_addr))) .and_then(self.map_err(TlsError::Service))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -143,19 +149,25 @@ mod openssl {
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod rustls { mod rustls {
use super::*; use super::*;
use std::io;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; rustls::{Acceptor, ServerConfig, TlsStream},
use std::{fmt, io}; TlsError,
};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Error>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@ -163,6 +175,7 @@ mod rustls {
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
@ -177,16 +190,14 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = (), InitError = (),
> { > {
pipeline_factory( Acceptor::new(config)
Acceptor::new(config) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| {
) let peer_addr = io.get_ref().0.peer_addr().ok();
.and_then(|io: TlsStream<TcpStream>| { ready(Ok((io, peer_addr)))
let peer_addr = io.get_ref().0.peer_addr().ok(); })
ready(Ok((io, peer_addr))) .and_then(self.map_err(TlsError::Service))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -241,16 +252,19 @@ where
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)> impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
for H1Service<T, S, B, X, U> for H1Service<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Error>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
@ -259,148 +273,50 @@ where
type Config = (); type Config = ();
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>; type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
type InitError = (); type InitError = ();
type Future = H1ServiceResponse<T, S, B, X, U>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
H1ServiceResponse { let service = self.srv.new_service(());
fut: self.srv.new_service(()), let expect = self.expect.new_service(());
fut_ex: Some(self.expect.new_service(())), let upgrade = self.upgrade.as_ref().map(|s| s.new_service(()));
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())), let on_connect_ext = self.on_connect_ext.clone();
expect: None, let cfg = self.cfg.clone();
upgrade: None,
on_connect_ext: self.on_connect_ext.clone(),
cfg: Some(self.cfg.clone()),
_phantom: PhantomData,
}
}
}
#[doc(hidden)] Box::pin(async move {
#[pin_project::pin_project] let expect = expect
pub struct H1ServiceResponse<T, S, B, X, U> .await
where .map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
S: ServiceFactory<Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
X: ServiceFactory<Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
#[pin]
fut: S::Future,
#[pin]
fut_ex: Option<X::Future>,
#[pin]
fut_upg: Option<U::Future>,
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: Option<ServiceConfig>,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> Future for H1ServiceResponse<T, S, B, X, U> let upgrade = match upgrade {
where Some(upgrade) => {
T: AsyncRead + AsyncWrite + Unpin, let upgrade = upgrade.await.map_err(|e| {
S: ServiceFactory<Request>, log::error!("Init http upgrade service error: {:?}", e)
S::Error: Into<Error>, })?;
S::Response: Into<Response<B>>, Some(upgrade)
S::InitError: fmt::Debug, }
B: MessageBody, None => None,
X: ServiceFactory<Request, Response = Request>, };
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
type Output = Result<H1ServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { let service = service
let mut this = self.as_mut().project(); .await
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
if let Some(fut) = this.fut_ex.as_pin_mut() { Ok(H1ServiceHandler::new(
let expect = ready!(fut cfg,
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.expect = Some(expect);
this.fut_ex.set(None);
}
if let Some(fut) = this.fut_upg.as_pin_mut() {
let upgrade = ready!(fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.upgrade = Some(upgrade);
this.fut_upg.set(None);
}
let result = ready!(this
.fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)));
Poll::Ready(result.map(|service| {
let this = self.as_mut().project();
H1ServiceHandler::new(
this.cfg.take().unwrap(),
service, service,
this.expect.take().unwrap(), expect,
this.upgrade.take(), upgrade,
this.on_connect_ext.clone(), on_connect_ext,
) ))
})) })
} }
} }
/// `Service` implementation for HTTP/1 transport /// `Service` implementation for HTTP/1 transport
pub struct H1ServiceHandler<T, S, B, X, U> pub type H1ServiceHandler<T, S, B, X, U> = HttpServiceHandler<T, S, B, X, U>;
where
S: Service<Request>,
X: Service<Request>,
U: Service<(Request, Framed<T, Codec>)>,
{
flow: Rc<HttpFlow<S, X, U>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> H1ServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
X: Service<Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
{
fn new(
cfg: ServiceConfig,
service: S,
expect: X,
upgrade: Option<U>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> H1ServiceHandler<T, S, B, X, U> {
H1ServiceHandler {
flow: HttpFlow::new(service, expect, upgrade),
cfg,
on_connect_ext,
_phantom: PhantomData,
}
}
}
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)> impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
for H1ServiceHandler<T, S, B, X, U> for HttpServiceHandler<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
@ -417,47 +333,10 @@ where
type Future = Dispatcher<T, S, B, X, U>; type Future = Dispatcher<T, S, B, X, U>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let ready = self self._poll_ready(cx).map_err(|e| {
.flow log::error!("HTTP/1 service readiness error: {:?}", e);
.expect DispatchError::Service(e)
.poll_ready(cx) })
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready();
let ready = self
.flow
.service
.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready;
let ready = if let Some(ref upg) = self.flow.upgrade {
upg.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready
} else {
ready
};
if ready {
Poll::Ready(Ok(()))
} else {
Poll::Pending
}
} }
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future { fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {

View File

@ -1,8 +1,6 @@
use std::task::Poll;
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use futures_util::future::{ready, Ready}; use futures_core::future::LocalBoxFuture;
use crate::error::Error; use crate::error::Error;
use crate::h1::Codec; use crate::h1::Codec;
@ -16,7 +14,7 @@ impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
type Config = (); type Config = ();
type Service = UpgradeHandler; type Service = UpgradeHandler;
type InitError = Error; type InitError = Error;
type Future = Ready<Result<Self::Service, Self::InitError>>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
unimplemented!() unimplemented!()
@ -26,11 +24,11 @@ impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler { impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler {
type Response = (); type Response = ();
type Error = Error; type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); actix_service::always_ready!();
fn call(&self, _: (Request, Framed<T, Codec>)) -> Self::Future { fn call(&self, _: (Request, Framed<T, Codec>)) -> Self::Future {
ready(Ok(())) unimplemented!()
} }
} }

View File

@ -5,8 +5,10 @@ use actix_codec::{AsyncRead, AsyncWrite};
use actix_service::Service; use actix_service::Service;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::ready; use futures_core::ready;
use h2::server::{Connection, SendResponse}; use h2::{
use h2::SendStream; server::{Connection, SendResponse},
SendStream,
};
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use log::{error, trace}; use log::{error, trace};
@ -250,8 +252,8 @@ where
} }
} }
Err(e) => { Err(err) => {
let res: Response = e.into().into(); let res = Response::from_error(err.into());
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
let mut send = send.take().unwrap(); let mut send = send.take().unwrap();

View File

@ -7,13 +7,13 @@ use std::{net, rc::Rc};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{ use actix_service::{
fn_factory, fn_service, pipeline_factory, IntoServiceFactory, Service, fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory,
ServiceFactory, ServiceFactoryExt as _,
}; };
use actix_utils::future::ready;
use bytes::Bytes; use bytes::Bytes;
use futures_core::ready; use futures_core::{future::LocalBoxFuture, ready};
use futures_util::future::ok; use h2::server::{handshake, Handshake};
use h2::server::{self, Handshake};
use log::error; use log::error;
use crate::body::MessageBody; use crate::body::MessageBody;
@ -65,6 +65,7 @@ where
impl<S, B> H2Service<TcpStream, S, B> impl<S, B> H2Service<TcpStream, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
@ -80,12 +81,12 @@ where
Error = DispatchError, Error = DispatchError,
InitError = S::InitError, InitError = S::InitError,
> { > {
pipeline_factory(fn_factory(|| async { fn_factory(|| {
Ok::<_, S::InitError>(fn_service(|io: TcpStream| { ready(Ok::<_, S::InitError>(fn_service(|io: TcpStream| {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
ok::<_, DispatchError>((io, peer_addr)) ready(Ok::<_, DispatchError>((io, peer_addr)))
})) })))
})) })
.and_then(self) .and_then(self)
} }
} }
@ -101,6 +102,7 @@ mod openssl {
impl<S, B> H2Service<TlsStream<TcpStream>, S, B> impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
@ -117,18 +119,18 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = S::InitError, InitError = S::InitError,
> { > {
pipeline_factory( Acceptor::new(acceptor)
Acceptor::new(acceptor) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(fn_factory(|| {
) ready(Ok::<_, S::InitError>(fn_service(
.and_then(fn_factory(|| { |io: TlsStream<TcpStream>| {
ok::<_, S::InitError>(fn_service(|io: TlsStream<TcpStream>| { let peer_addr = io.get_ref().peer_addr().ok();
let peer_addr = io.get_ref().peer_addr().ok(); ready(Ok((io, peer_addr)))
ok((io, peer_addr)) },
)))
})) }))
})) .and_then(self.map_err(TlsError::Service))
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -144,6 +146,7 @@ mod rustls {
impl<S, B> H2Service<TlsStream<TcpStream>, S, B> impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
@ -163,26 +166,27 @@ mod rustls {
let protos = vec!["h2".to_string().into()]; let protos = vec!["h2".to_string().into()];
config.set_protocols(&protos); config.set_protocols(&protos);
pipeline_factory( Acceptor::new(config)
Acceptor::new(config) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(fn_factory(|| {
) ready(Ok::<_, S::InitError>(fn_service(
.and_then(fn_factory(|| { |io: TlsStream<TcpStream>| {
ok::<_, S::InitError>(fn_service(|io: TlsStream<TcpStream>| { let peer_addr = io.get_ref().0.peer_addr().ok();
let peer_addr = io.get_ref().0.peer_addr().ok(); ready(Ok((io, peer_addr)))
ok((io, peer_addr)) },
)))
})) }))
})) .and_then(self.map_err(TlsError::Service))
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B> impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
@ -193,52 +197,16 @@ where
type Config = (); type Config = ();
type Service = H2ServiceHandler<T, S::Service, B>; type Service = H2ServiceHandler<T, S::Service, B>;
type InitError = S::InitError; type InitError = S::InitError;
type Future = H2ServiceResponse<T, S, B>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
H2ServiceResponse { let service = self.srv.new_service(());
fut: self.srv.new_service(()), let cfg = self.cfg.clone();
cfg: Some(self.cfg.clone()), let on_connect_ext = self.on_connect_ext.clone();
on_connect_ext: self.on_connect_ext.clone(),
_phantom: PhantomData,
}
}
}
#[doc(hidden)] Box::pin(async move {
#[pin_project::pin_project] let service = service.await?;
pub struct H2ServiceResponse<T, S, B> Ok(H2ServiceHandler::new(cfg, on_connect_ext, service))
where
S: ServiceFactory<Request>,
{
#[pin]
fut: S::Future,
cfg: Option<ServiceConfig>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>,
}
impl<T, S, B> Future for H2ServiceResponse<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
type Output = Result<H2ServiceHandler<T, S::Service, B>, S::InitError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project();
this.fut.poll(cx).map_ok(|service| {
let this = self.as_mut().project();
H2ServiceHandler::new(
this.cfg.take().unwrap(),
this.on_connect_ext.clone(),
service,
)
}) })
} }
} }
@ -307,7 +275,7 @@ where
Some(self.cfg.clone()), Some(self.cfg.clone()),
addr, addr,
on_connect_data, on_connect_data,
server::handshake(io), handshake(io),
), ),
} }
} }

View File

@ -1,9 +1,6 @@
//! Typed HTTP headers, pre-defined `HeaderName`s, traits for parsing and conversion, and other //! Typed HTTP headers, pre-defined `HeaderName`s, traits for parsing and conversion, and other
//! header utility methods. //! header utility methods.
use std::fmt;
use bytes::{Bytes, BytesMut};
use percent_encoding::{AsciiSet, CONTROLS}; use percent_encoding::{AsciiSet, CONTROLS};
pub use http::header::*; pub use http::header::*;
@ -16,11 +13,9 @@ mod into_pair;
mod into_value; mod into_value;
mod utils; mod utils;
mod common;
pub(crate) mod map; pub(crate) mod map;
mod shared; mod shared;
pub use self::common::*;
#[doc(hidden)] #[doc(hidden)]
pub use self::shared::*; pub use self::shared::*;
@ -41,34 +36,6 @@ pub trait Header: IntoHeaderValue {
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>; fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
} }
#[derive(Debug, Default)]
pub(crate) struct Writer {
buf: BytesMut,
}
impl Writer {
fn new() -> Writer {
Writer::default()
}
fn take(&mut self) -> Bytes {
self.buf.split().freeze()
}
}
impl fmt::Write for Writer {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.buf.extend_from_slice(s.as_bytes());
Ok(())
}
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
fmt::write(self, args)
}
}
/// Convert `http::HeaderMap` to our `HeaderMap`. /// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap { impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> HeaderMap { fn from(mut map: http::HeaderMap) -> HeaderMap {

View File

@ -88,9 +88,9 @@ pub fn parse_extended_value(
}; };
Ok(ExtendedValue { Ok(ExtendedValue {
value,
charset, charset,
language_tag, language_tag,
value,
}) })
} }

View File

@ -1,18 +1,20 @@
use std::fmt::{self, Display}; use std::{
use std::io::Write; fmt,
use std::str::FromStr; io::Write,
use std::time::{SystemTime, UNIX_EPOCH}; str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
use bytes::buf::BufMut; use bytes::buf::BufMut;
use bytes::BytesMut; use bytes::BytesMut;
use http::header::{HeaderValue, InvalidHeaderValue}; use http::header::{HeaderValue, InvalidHeaderValue};
use time::{offset, OffsetDateTime, PrimitiveDateTime}; use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
use crate::error::ParseError; use crate::error::ParseError;
use crate::header::IntoHeaderValue; use crate::header::IntoHeaderValue;
use crate::time_parser; use crate::time_parser;
/// A timestamp with HTTP formatting and parsing /// A timestamp with HTTP formatting and parsing.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpDate(OffsetDateTime); pub struct HttpDate(OffsetDateTime);
@ -27,18 +29,12 @@ impl FromStr for HttpDate {
} }
} }
impl Display for HttpDate { impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f) fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f)
} }
} }
impl From<OffsetDateTime> for HttpDate {
fn from(dt: OffsetDateTime) -> HttpDate {
HttpDate(dt)
}
}
impl From<SystemTime> for HttpDate { impl From<SystemTime> for HttpDate {
fn from(sys: SystemTime) -> HttpDate { fn from(sys: SystemTime) -> HttpDate {
HttpDate(PrimitiveDateTime::from(sys).assume_utc()) HttpDate(PrimitiveDateTime::from(sys).assume_utc())
@ -54,7 +50,7 @@ impl IntoHeaderValue for HttpDate {
wrt, wrt,
"{}", "{}",
self.0 self.0
.to_offset(offset!(UTC)) .to_offset(UtcOffset::UTC)
.format("%a, %d %b %Y %H:%M:%S GMT") .format("%a, %d %b %Y %H:%M:%S GMT")
) )
.unwrap(); .unwrap();

View File

@ -1,15 +1,13 @@
//! Originally taken from `hyper::header::shared`. //! Originally taken from `hyper::header::shared`.
mod charset; mod charset;
mod encoding; mod content_encoding;
mod entity;
mod extended; mod extended;
mod httpdate; mod httpdate;
mod quality_item; mod quality_item;
pub use self::charset::Charset; pub use self::charset::Charset;
pub use self::encoding::Encoding; pub use self::content_encoding::ContentEncoding;
pub use self::entity::EntityTag;
pub use self::extended::{parse_extended_value, ExtendedValue}; pub use self::extended::{parse_extended_value, ExtendedValue};
pub use self::httpdate::HttpDate; pub use self::httpdate::HttpDate;
pub use self::quality_item::{q, qitem, Quality, QualityItem}; pub use self::quality_item::{q, qitem, Quality, QualityItem};

View File

@ -193,21 +193,69 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::encoding::*;
use super::*; use super::*;
// copy of encoding from actix-web headers
#[derive(Clone, PartialEq, Debug)]
pub enum Encoding {
Chunked,
Brotli,
Gzip,
Deflate,
Compress,
Identity,
Trailers,
EncodingExt(String),
}
impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Encoding::*;
f.write_str(match *self {
Chunked => "chunked",
Brotli => "br",
Gzip => "gzip",
Deflate => "deflate",
Compress => "compress",
Identity => "identity",
Trailers => "trailers",
EncodingExt(ref s) => s.as_ref(),
})
}
}
impl str::FromStr for Encoding {
type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
use Encoding::*;
match s {
"chunked" => Ok(Chunked),
"br" => Ok(Brotli),
"deflate" => Ok(Deflate),
"gzip" => Ok(Gzip),
"compress" => Ok(Compress),
"identity" => Ok(Identity),
"trailers" => Ok(Trailers),
_ => Ok(EncodingExt(s.to_owned())),
}
}
}
#[test] #[test]
fn test_quality_item_fmt_q_1() { fn test_quality_item_fmt_q_1() {
use Encoding::*;
let x = qitem(Chunked); let x = qitem(Chunked);
assert_eq!(format!("{}", x), "chunked"); assert_eq!(format!("{}", x), "chunked");
} }
#[test] #[test]
fn test_quality_item_fmt_q_0001() { fn test_quality_item_fmt_q_0001() {
use Encoding::*;
let x = QualityItem::new(Chunked, Quality(1)); let x = QualityItem::new(Chunked, Quality(1));
assert_eq!(format!("{}", x), "chunked; q=0.001"); assert_eq!(format!("{}", x), "chunked; q=0.001");
} }
#[test] #[test]
fn test_quality_item_fmt_q_05() { fn test_quality_item_fmt_q_05() {
use Encoding::*;
// Custom value // Custom value
let x = QualityItem { let x = QualityItem {
item: EncodingExt("identity".to_owned()), item: EncodingExt("identity".to_owned()),
@ -218,6 +266,7 @@ mod tests {
#[test] #[test]
fn test_quality_item_fmt_q_0() { fn test_quality_item_fmt_q_0() {
use Encoding::*;
// Custom value // Custom value
let x = QualityItem { let x = QualityItem {
item: EncodingExt("identity".to_owned()), item: EncodingExt("identity".to_owned()),
@ -228,6 +277,7 @@ mod tests {
#[test] #[test]
fn test_quality_item_from_str1() { fn test_quality_item_from_str1() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "chunked".parse(); let x: Result<QualityItem<Encoding>, _> = "chunked".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -237,8 +287,10 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str2() { fn test_quality_item_from_str2() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "chunked; q=1".parse(); let x: Result<QualityItem<Encoding>, _> = "chunked; q=1".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -248,8 +300,10 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str3() { fn test_quality_item_from_str3() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.5".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.5".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -259,8 +313,10 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str4() { fn test_quality_item_from_str4() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.273".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.273".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -270,16 +326,19 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str5() { fn test_quality_item_from_str5() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.2739999".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.2739999".parse();
assert!(x.is_err()); assert!(x.is_err());
} }
#[test] #[test]
fn test_quality_item_from_str6() { fn test_quality_item_from_str6() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=2".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=2".parse();
assert!(x.is_err()); assert!(x.is_err());
} }
#[test] #[test]
fn test_quality_item_ordering() { fn test_quality_item_ordering() {
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap(); let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();

View File

@ -1,7 +1,6 @@
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
use http::HeaderValue; use super::HeaderValue;
use crate::{error::ParseError, header::HTTP_VALUE}; use crate::{error::ParseError, header::HTTP_VALUE};
/// Reads a comma-delimited raw header into a Vec. /// Reads a comma-delimited raw header into a Vec.

View File

@ -1,15 +1,15 @@
use std::io; use std::io;
use bytes::{BufMut, BytesMut}; use bytes::BufMut;
use http::Version; use http::Version;
const DIGITS_START: u8 = b'0'; const DIGITS_START: u8 = b'0';
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { pub(crate) fn write_status_line<B: BufMut>(version: Version, n: u16, buf: &mut B) {
match version { match version {
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), Version::HTTP_11 => buf.put_slice(b"HTTP/1.1 "),
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), Version::HTTP_10 => buf.put_slice(b"HTTP/1.0 "),
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), Version::HTTP_09 => buf.put_slice(b"HTTP/0.9 "),
_ => { _ => {
// other HTTP version handlers do not use this method // other HTTP version handlers do not use this method
} }
@ -19,33 +19,36 @@ pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut)
let d10 = ((n / 10) % 10) as u8; let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8; let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d100); buf.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10); buf.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1); buf.put_u8(DIGITS_START + d1);
// trailing space before reason // trailing space before reason
bytes.put_u8(b' '); buf.put_u8(b' ');
} }
/// NOTE: bytes object has to contain enough space /// NOTE: bytes object has to contain enough space
pub fn write_content_length(n: u64, bytes: &mut BytesMut) { pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B) {
if n == 0 { if n == 0 {
bytes.put_slice(b"\r\ncontent-length: 0\r\n"); buf.put_slice(b"\r\ncontent-length: 0\r\n");
return; return;
} }
let mut buf = itoa::Buffer::new(); let mut buffer = itoa::Buffer::new();
bytes.put_slice(b"\r\ncontent-length: "); buf.put_slice(b"\r\ncontent-length: ");
bytes.put_slice(buf.format(n).as_bytes()); buf.put_slice(buffer.format(n).as_bytes());
bytes.put_slice(b"\r\n"); buf.put_slice(b"\r\n");
} }
pub(crate) struct Writer<'a>(pub &'a mut BytesMut); pub(crate) struct Writer<'a, B>(pub &'a mut B);
impl<'a> io::Write for Writer<'a> { impl<'a, B> io::Write for Writer<'a, B>
where
B: BufMut,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf); self.0.put_slice(buf);
Ok(buf.len()) Ok(buf.len())
} }
@ -58,6 +61,8 @@ impl<'a> io::Write for Writer<'a> {
mod tests { mod tests {
use std::str::from_utf8; use std::str::from_utf8;
use bytes::BytesMut;
use super::*; use super::*;
#[test] #[test]

View File

@ -9,11 +9,6 @@ use crate::error::{ContentTypeError, ParseError};
use crate::extensions::Extensions; use crate::extensions::Extensions;
use crate::header::{Header, HeaderMap}; use crate::header::{Header, HeaderMap};
use crate::payload::Payload; use crate::payload::Payload;
#[cfg(feature = "cookies")]
use crate::{cookie::Cookie, error::CookieParseError};
#[cfg(feature = "cookies")]
struct Cookies(Vec<Cookie<'static>>);
/// Trait that implements general purpose operations on HTTP messages. /// Trait that implements general purpose operations on HTTP messages.
pub trait HttpMessage: Sized { pub trait HttpMessage: Sized {
@ -104,41 +99,6 @@ pub trait HttpMessage: Sized {
Ok(false) Ok(false)
} }
} }
/// Load request cookies.
#[cfg(feature = "cookies")]
fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
if self.extensions().get::<Cookies>().is_none() {
let mut cookies = Vec::new();
for hdr in self.headers().get_all(header::COOKIE) {
let s =
str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
for cookie_str in s.split(';').map(|s| s.trim()) {
if !cookie_str.is_empty() {
cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned());
}
}
}
self.extensions_mut().insert(Cookies(cookies));
}
Ok(Ref::map(self.extensions(), |ext| {
&ext.get::<Cookies>().unwrap().0
}))
}
/// Return request cookie.
#[cfg(feature = "cookies")]
fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
if let Ok(cookies) = self.cookies() {
for cookie in cookies.iter() {
if cookie.name() == name {
return Some(cookie.to_owned());
}
}
}
None
}
} }
impl<'a, T> HttpMessage for &'a mut T impl<'a, T> HttpMessage for &'a mut T

View File

@ -6,13 +6,10 @@
//! | `openssl` | TLS support via [OpenSSL]. | //! | `openssl` | TLS support via [OpenSSL]. |
//! | `rustls` | TLS support via [rustls]. | //! | `rustls` | TLS support via [rustls]. |
//! | `compress` | Payload compression support. (Deflate, Gzip & Brotli) | //! | `compress` | Payload compression support. (Deflate, Gzip & Brotli) |
//! | `cookies` | Support for cookies backed by the [cookie] crate. |
//! | `secure-cookies` | Adds for secure cookies. Enables `cookies` feature. |
//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | //! | `trust-dns` | Use [trust-dns] as the client DNS resolver. |
//! //!
//! [OpenSSL]: https://crates.io/crates/openssl //! [OpenSSL]: https://crates.io/crates/openssl
//! [rustls]: https://crates.io/crates/rustls //! [rustls]: https://crates.io/crates/rustls
//! [cookie]: https://crates.io/crates/cookie
//! [trust-dns]: https://crates.io/crates/trust-dns //! [trust-dns]: https://crates.io/crates/trust-dns
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
@ -40,12 +37,12 @@ pub mod encoding;
mod extensions; mod extensions;
mod header; mod header;
mod helpers; mod helpers;
mod http_codes;
mod http_message; mod http_message;
mod message; mod message;
mod payload; mod payload;
mod request; mod request;
mod response; mod response;
mod response_builder;
mod service; mod service;
mod time_parser; mod time_parser;
@ -55,9 +52,6 @@ pub mod h2;
pub mod test; pub mod test;
pub mod ws; pub mod ws;
#[cfg(feature = "cookies")]
pub use cookie;
pub use self::builder::HttpServiceBuilder; pub use self::builder::HttpServiceBuilder;
pub use self::config::{KeepAlive, ServiceConfig}; pub use self::config::{KeepAlive, ServiceConfig};
pub use self::error::{Error, ResponseError, Result}; pub use self::error::{Error, ResponseError, Result};
@ -66,7 +60,8 @@ pub use self::http_message::HttpMessage;
pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead}; pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead};
pub use self::payload::{Payload, PayloadStream}; pub use self::payload::{Payload, PayloadStream};
pub use self::request::Request; pub use self::request::Request;
pub use self::response::{Response, ResponseBuilder}; pub use self::response::Response;
pub use self::response_builder::ResponseBuilder;
pub use self::service::HttpService; pub use self::service::HttpService;
pub mod http { pub mod http {
@ -78,8 +73,6 @@ pub mod http {
pub use http::{uri, Error, Uri}; pub use http::{uri, Error, Uri};
pub use http::{Method, StatusCode, Version}; pub use http::{Method, StatusCode, Version};
#[cfg(feature = "cookies")]
pub use crate::cookie::{Cookie, CookieBuilder};
pub use crate::header::HeaderMap; pub use crate::header::HeaderMap;
/// A collection of HTTP headers and helpers. /// A collection of HTTP headers and helpers.

View File

@ -70,6 +70,7 @@ macro_rules! downcast {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#![allow(clippy::upper_case_acronyms)]
trait MB { trait MB {
downcast_get_type_id!(); downcast_get_type_id!();

View File

@ -345,8 +345,8 @@ impl ResponseHead {
} }
pub struct Message<T: Head> { pub struct Message<T: Head> {
// Rc here should not be cloned by anyone. /// Rc here should not be cloned by anyone.
// It's used to reuse allocation of T and no shared ownership is allowed. /// It's used to reuse allocation of T and no shared ownership is allowed.
head: Rc<T>, head: Rc<T>,
} }

View File

@ -2,16 +2,18 @@
use std::{ use std::{
cell::{Ref, RefMut}, cell::{Ref, RefMut},
fmt, net, fmt, net, str,
}; };
use http::{header, Method, Uri, Version}; use http::{header, Method, Uri, Version};
use crate::extensions::Extensions; use crate::{
use crate::header::HeaderMap; extensions::Extensions,
use crate::message::{Message, RequestHead}; header::HeaderMap,
use crate::payload::{Payload, PayloadStream}; message::{Message, RequestHead},
use crate::HttpMessage; payload::{Payload, PayloadStream},
HttpMessage,
};
/// Request /// Request
pub struct Request<P = PayloadStream> { pub struct Request<P = PayloadStream> {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,468 @@
//! HTTP response builder.
use std::{
cell::{Ref, RefMut},
fmt,
future::Future,
pin::Pin,
str,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::Stream;
use crate::{
body::{Body, BodyStream, ResponseBody},
error::Error,
extensions::Extensions,
header::{IntoHeaderPair, IntoHeaderValue},
http::{header, Error as HttpError, StatusCode},
message::{BoxedResponseHead, ConnectionType, ResponseHead},
Response,
};
/// An HTTP response builder.
///
/// Used to construct an instance of `Response` using a builder pattern. Response builders are often
/// created using [`Response::build`].
///
/// # Examples
/// ```
/// use actix_http::{Response, ResponseBuilder, body, http::StatusCode, http::header};
///
/// # actix_rt::System::new().block_on(async {
/// let mut res: Response<_> = Response::build(StatusCode::OK)
/// .content_type(mime::APPLICATION_JSON)
/// .insert_header((header::SERVER, "my-app/1.0"))
/// .append_header((header::SET_COOKIE, "a=1"))
/// .append_header((header::SET_COOKIE, "b=2"))
/// .body("1234");
///
/// assert_eq!(res.status(), StatusCode::OK);
/// assert_eq!(body::to_bytes(res.take_body()).await.unwrap(), &b"1234"[..]);
///
/// assert!(res.headers().contains_key("server"));
/// assert_eq!(res.headers().get_all("set-cookie").count(), 2);
/// # })
/// ```
pub struct ResponseBuilder {
head: Option<BoxedResponseHead>,
err: Option<HttpError>,
}
impl ResponseBuilder {
/// Create response builder
///
/// # Examples
/// ```
/// use actix_http::{Response, ResponseBuilder, http::StatusCode};
///
/// let res: Response<_> = ResponseBuilder::default().finish();
/// assert_eq!(res.status(), StatusCode::OK);
/// ```
#[inline]
pub fn new(status: StatusCode) -> Self {
ResponseBuilder {
head: Some(BoxedResponseHead::new(status)),
err: None,
}
}
/// Set HTTP status code of this response.
///
/// # Examples
/// ```
/// use actix_http::{ResponseBuilder, http::StatusCode};
///
/// let res = ResponseBuilder::default().status(StatusCode::NOT_FOUND).finish();
/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
/// ```
#[inline]
pub fn status(&mut self, status: StatusCode) -> &mut Self {
if let Some(parts) = self.inner() {
parts.status = status;
}
self
}
/// Insert a header, replacing any that were set with an equivalent field name.
///
/// # Examples
/// ```
/// use actix_http::{ResponseBuilder, http::header};
///
/// let res = ResponseBuilder::default()
/// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON))
/// .insert_header(("X-TEST", "value"))
/// .finish();
///
/// assert!(res.headers().contains_key("content-type"));
/// assert!(res.headers().contains_key("x-test"));
/// ```
pub fn insert_header<H>(&mut self, header: H) -> &mut Self
where
H: IntoHeaderPair,
{
if let Some(parts) = self.inner() {
match header.try_into_header_pair() {
Ok((key, value)) => {
parts.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Append a header, keeping any that were set with an equivalent field name.
///
/// # Examples
/// ```
/// use actix_http::{ResponseBuilder, http::header};
///
/// let res = ResponseBuilder::default()
/// .append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON))
/// .append_header(("X-TEST", "value1"))
/// .append_header(("X-TEST", "value2"))
/// .finish();
///
/// assert_eq!(res.headers().get_all("content-type").count(), 1);
/// assert_eq!(res.headers().get_all("x-test").count(), 2);
/// ```
pub fn append_header<H>(&mut self, header: H) -> &mut Self
where
H: IntoHeaderPair,
{
if let Some(parts) = self.inner() {
match header.try_into_header_pair() {
Ok((key, value)) => parts.headers.append(key, value),
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Set the custom reason for the response.
#[inline]
pub fn reason(&mut self, reason: &'static str) -> &mut Self {
if let Some(parts) = self.inner() {
parts.reason = Some(reason);
}
self
}
/// Set connection type to KeepAlive
#[inline]
pub fn keep_alive(&mut self) -> &mut Self {
if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::KeepAlive);
}
self
}
/// Set connection type to Upgrade
#[inline]
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
where
V: IntoHeaderValue,
{
if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::Upgrade);
}
if let Ok(value) = value.try_into_value() {
self.insert_header((header::UPGRADE, value));
}
self
}
/// Force close connection, even if it is marked as keep-alive
#[inline]
pub fn force_close(&mut self) -> &mut Self {
if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::Close);
}
self
}
/// Disable chunked transfer encoding for HTTP/1.1 streaming responses.
#[inline]
pub fn no_chunking(&mut self, len: u64) -> &mut Self {
let mut buf = itoa::Buffer::new();
self.insert_header((header::CONTENT_LENGTH, buf.format(len)));
if let Some(parts) = self.inner() {
parts.no_chunking(true);
}
self
}
/// Set response content type.
#[inline]
pub fn content_type<V>(&mut self, value: V) -> &mut Self
where
V: IntoHeaderValue,
{
if let Some(parts) = self.inner() {
match value.try_into_value() {
Ok(value) => {
parts.headers.insert(header::CONTENT_TYPE, value);
}
Err(e) => self.err = Some(e.into()),
};
}
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.
///
/// This `ResponseBuilder` will be left in a useless state.
#[inline]
pub fn body<B: Into<Body>>(&mut self, body: B) -> Response<Body> {
self.message_body(body.into())
}
/// Generate response with a body.
///
/// This `ResponseBuilder` will be left in a useless state.
pub fn message_body<B>(&mut self, body: B) -> Response<B> {
if let Some(e) = self.err.take() {
return Response::from(Error::from(e)).into_body();
}
let response = self.head.take().expect("cannot reuse response builder");
Response {
head: response,
body: ResponseBody::Body(body),
error: None,
}
}
/// Generate response with a streaming body.
///
/// This `ResponseBuilder` will be left in a useless state.
#[inline]
pub fn streaming<S, E>(&mut self, stream: S) -> Response<Body>
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Into<Error> + 'static,
{
self.body(Body::from_message(BodyStream::new(stream)))
}
/// Generate response with an empty body.
///
/// This `ResponseBuilder` will be left in a useless state.
#[inline]
pub fn finish(&mut self) -> Response<Body> {
self.body(Body::Empty)
}
/// Create an owned `ResponseBuilder`, leaving the original in a useless state.
pub fn take(&mut self) -> ResponseBuilder {
ResponseBuilder {
head: self.head.take(),
err: self.err.take(),
}
}
/// Get access to the inner response head if there has been no error.
fn inner(&mut self) -> Option<&mut ResponseHead> {
if self.err.is_some() {
return None;
}
self.head.as_mut().map(|r| &mut **r)
}
}
impl Default for ResponseBuilder {
fn default() -> Self {
Self::new(StatusCode::OK)
}
}
/// Convert `Response` to a `ResponseBuilder`. Body get dropped.
impl<B> From<Response<B>> for ResponseBuilder {
fn from(res: Response<B>) -> ResponseBuilder {
ResponseBuilder {
head: Some(res.head),
err: None,
}
}
}
/// Convert `ResponseHead` to a `ResponseBuilder`
impl<'a> From<&'a ResponseHead> for ResponseBuilder {
fn from(head: &'a ResponseHead) -> ResponseBuilder {
let mut msg = BoxedResponseHead::new(head.status);
msg.version = head.version;
msg.reason = head.reason;
for (k, v) in head.headers.iter() {
msg.headers.append(k.clone(), v.clone());
}
msg.no_chunking(!head.chunked());
ResponseBuilder {
head: Some(msg),
err: None,
}
}
}
impl Future for ResponseBuilder {
type Output = Result<Response<Body>, Error>;
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(Ok(self.finish()))
}
}
impl fmt::Debug for ResponseBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let head = self.head.as_ref().unwrap();
let res = writeln!(
f,
"\nResponseBuilder {:?} {}{}",
head.version,
head.status,
head.reason.unwrap_or(""),
);
let _ = writeln!(f, " headers:");
for (key, val) in head.headers.iter() {
let _ = writeln!(f, " {:?}: {:?}", key, val);
}
res
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::body::Body;
use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
#[test]
fn test_basic_builder() {
let resp = Response::build(StatusCode::OK)
.insert_header(("X-TEST", "value"))
.finish();
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_upgrade() {
let resp = Response::build(StatusCode::OK)
.upgrade("websocket")
.finish();
assert!(resp.upgrade());
assert_eq!(
resp.headers().get(header::UPGRADE).unwrap(),
HeaderValue::from_static("websocket")
);
}
#[test]
fn test_force_close() {
let resp = Response::build(StatusCode::OK).force_close().finish();
assert!(!resp.keep_alive())
}
#[test]
fn test_content_type() {
let resp = Response::build(StatusCode::OK)
.content_type("text/plain")
.body(Body::Empty);
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain")
}
#[test]
fn test_into_builder() {
let mut resp: Response<Body> = "test".into();
assert_eq!(resp.status(), StatusCode::OK);
resp.headers_mut().insert(
HeaderName::from_static("cookie"),
HeaderValue::from_static("cookie1=val100"),
);
let mut builder: ResponseBuilder = resp.into();
let resp = builder.status(StatusCode::BAD_REQUEST).finish();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let cookie = resp.headers().get_all("Cookie").next().unwrap();
assert_eq!(cookie.to_str().unwrap(), "cookie1=val100");
}
#[test]
fn response_builder_header_insert_kv() {
let mut res = Response::build(StatusCode::OK);
res.insert_header(("Content-Type", "application/octet-stream"));
let res = res.finish();
assert_eq!(
res.headers().get("Content-Type"),
Some(&HeaderValue::from_static("application/octet-stream"))
);
}
#[test]
fn response_builder_header_insert_typed() {
let mut res = Response::build(StatusCode::OK);
res.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
let res = res.finish();
assert_eq!(
res.headers().get("Content-Type"),
Some(&HeaderValue::from_static("application/octet-stream"))
);
}
#[test]
fn response_builder_header_append_kv() {
let mut res = Response::build(StatusCode::OK);
res.append_header(("Content-Type", "application/octet-stream"));
res.append_header(("Content-Type", "application/json"));
let res = res.finish();
let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
assert_eq!(headers.len(), 2);
assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
assert!(headers.contains(&HeaderValue::from_static("application/json")));
}
#[test]
fn response_builder_header_append_typed() {
let mut res = Response::build(StatusCode::OK);
res.append_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
res.append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON));
let res = res.finish();
let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
assert_eq!(headers.len(), 2);
assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
assert!(headers.contains(&HeaderValue::from_static("application/json")));
}
}

View File

@ -1,14 +1,21 @@
use std::marker::PhantomData; use std::{
use std::pin::Pin; fmt,
use std::task::{Context, Poll}; future::Future,
use std::{fmt, net, rc::Rc}; marker::PhantomData,
net,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; use actix_service::{
fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
};
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Future}; use futures_core::{future::LocalBoxFuture, ready};
use h2::server::{self, Handshake}; use h2::server::{handshake, Handshake};
use pin_project::pin_project; use pin_project::pin_project;
use crate::body::MessageBody; use crate::body::MessageBody;
@ -102,7 +109,6 @@ where
X1: ServiceFactory<Request, Config = (), Response = Request>, X1: ServiceFactory<Request, Config = (), Response = Request>,
X1::Error: Into<Error>, X1::Error: Into<Error>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
<X1::Service as Service<Request>>::Future: 'static,
{ {
HttpService { HttpService {
expect, expect,
@ -123,7 +129,6 @@ where
U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>, U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U1::Error: fmt::Display, U1::Error: fmt::Display,
U1::InitError: fmt::Debug, U1::InitError: fmt::Debug,
<U1::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
{ {
HttpService { HttpService {
upgrade, upgrade,
@ -145,23 +150,27 @@ where
impl<S, B, X, U> HttpService<TcpStream, S, B, X, U> impl<S, B, X, U> HttpService<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TcpStream, h1::Codec>), (Request, Framed<TcpStream, h1::Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<TcpStream, h1::Codec>)>>::Future: 'static,
{ {
/// Create simple tcp stream service /// Create simple tcp stream service
pub fn tcp( pub fn tcp(
@ -173,7 +182,7 @@ where
Error = DispatchError, Error = DispatchError,
InitError = (), InitError = (),
> { > {
pipeline_factory(|io: TcpStream| async { fn_service(|io: TcpStream| async {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
Ok((io, Protocol::Http1, peer_addr)) Ok((io, Protocol::Http1, peer_addr))
}) })
@ -183,31 +192,36 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use super::*;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
use actix_tls::accept::TlsError; use actix_tls::accept::TlsError;
use super::*;
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>), (Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<TlsStream<TcpStream>, h1::Codec>)>>::Future: 'static,
{ {
/// Create openssl based service /// Create openssl based service
pub fn openssl( pub fn openssl(
@ -220,25 +234,23 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = (), InitError = (),
> { > {
pipeline_factory( Acceptor::new(acceptor)
Acceptor::new(acceptor) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| async {
) let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() {
.and_then(|io: TlsStream<TcpStream>| async { if protos.windows(2).any(|window| window == b"h2") {
let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() { Protocol::Http2
if protos.windows(2).any(|window| window == b"h2") { } else {
Protocol::Http2 Protocol::Http1
}
} else { } else {
Protocol::Http1 Protocol::Http1
} };
} else { let peer_addr = io.get_ref().peer_addr().ok();
Protocol::Http1 Ok((io, proto, peer_addr))
}; })
let peer_addr = io.get_ref().peer_addr().ok(); .and_then(self.map_err(TlsError::Service))
Ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -256,25 +268,29 @@ mod rustls {
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>), (Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<TlsStream<TcpStream>, h1::Codec>)>>::Future: 'static,
{ {
/// Create openssl based service /// Create rustls based service
pub fn rustls( pub fn rustls(
self, self,
mut config: ServerConfig, mut config: ServerConfig,
@ -288,25 +304,24 @@ mod rustls {
let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()];
config.set_protocols(&protos); config.set_protocols(&protos);
pipeline_factory( Acceptor::new(config)
Acceptor::new(config) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| async {
) let proto = if let Some(protos) = io.get_ref().1.get_alpn_protocol()
.and_then(|io: TlsStream<TcpStream>| async { {
let proto = if let Some(protos) = io.get_ref().1.get_alpn_protocol() { if protos.windows(2).any(|window| window == b"h2") {
if protos.windows(2).any(|window| window == b"h2") { Protocol::Http2
Protocol::Http2 } else {
Protocol::Http1
}
} else { } else {
Protocol::Http1 Protocol::Http1
} };
} else { let peer_addr = io.get_ref().0.peer_addr().ok();
Protocol::Http1 Ok((io, proto, peer_addr))
}; })
let peer_addr = io.get_ref().0.peer_addr().ok(); .and_then(self.map_err(TlsError::Service))
Ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -314,137 +329,121 @@ mod rustls {
impl<T, S, B, X, U> ServiceFactory<(T, Protocol, Option<net::SocketAddr>)> impl<T, S, B, X, U> ServiceFactory<(T, Protocol, Option<net::SocketAddr>)>
for HttpService<T, S, B, X, U> for HttpService<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Config = (); type Config = ();
type Service = HttpServiceHandler<T, S::Service, B, X::Service, U::Service>; type Service = HttpServiceHandler<T, S::Service, B, X::Service, U::Service>;
type InitError = (); type InitError = ();
type Future = HttpServiceResponse<T, S, B, X, U>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
HttpServiceResponse { let service = self.srv.new_service(());
fut: self.srv.new_service(()), let expect = self.expect.new_service(());
fut_ex: Some(self.expect.new_service(())), let upgrade = self.upgrade.as_ref().map(|s| s.new_service(()));
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())), let on_connect_ext = self.on_connect_ext.clone();
expect: None, let cfg = self.cfg.clone();
upgrade: None,
on_connect_ext: self.on_connect_ext.clone(),
cfg: self.cfg.clone(),
_phantom: PhantomData,
}
}
}
#[doc(hidden)] Box::pin(async move {
#[pin_project] let expect = expect
pub struct HttpServiceResponse<T, S, B, X, U> .await
where .map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
S: ServiceFactory<Request>,
X: ServiceFactory<Request>,
U: ServiceFactory<(Request, Framed<T, h1::Codec>)>,
{
#[pin]
fut: S::Future,
#[pin]
fut_ex: Option<X::Future>,
#[pin]
fut_upg: Option<U::Future>,
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> Future for HttpServiceResponse<T, S, B, X, U> let upgrade = match upgrade {
where Some(upgrade) => {
T: AsyncRead + AsyncWrite + Unpin, let upgrade = upgrade.await.map_err(|e| {
S: ServiceFactory<Request>, log::error!("Init http upgrade service error: {:?}", e)
S::Error: Into<Error> + 'static, })?;
S::InitError: fmt::Debug, Some(upgrade)
S::Response: Into<Response<B>> + 'static, }
<S::Service as Service<Request>>::Future: 'static, None => None,
B: MessageBody + 'static, };
X: ServiceFactory<Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
{
type Output =
Result<HttpServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { let service = service
let mut this = self.as_mut().project(); .await
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
if let Some(fut) = this.fut_ex.as_pin_mut() { Ok(HttpServiceHandler::new(
let expect = ready!(fut cfg,
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.expect = Some(expect);
this.fut_ex.set(None);
}
if let Some(fut) = this.fut_upg.as_pin_mut() {
let upgrade = ready!(fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.upgrade = Some(upgrade);
this.fut_upg.set(None);
}
let result = ready!(this
.fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)));
Poll::Ready(result.map(|service| {
let this = self.as_mut().project();
HttpServiceHandler::new(
this.cfg.clone(),
service, service,
this.expect.take().unwrap(), expect,
this.upgrade.take(), upgrade,
this.on_connect_ext.clone(), on_connect_ext,
) ))
})) })
} }
} }
/// `Service` implementation for HTTP transport /// `Service` implementation for HTTP/1 and HTTP/2 transport
pub struct HttpServiceHandler<T, S, B, X, U> pub struct HttpServiceHandler<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
X: Service<Request>, X: Service<Request>,
U: Service<(Request, Framed<T, h1::Codec>)>, U: Service<(Request, Framed<T, h1::Codec>)>,
{ {
flow: Rc<HttpFlow<S, X, U>>, pub(super) flow: Rc<HttpFlow<S, X, U>>,
cfg: ServiceConfig, pub(super) cfg: ServiceConfig,
on_connect_ext: Option<Rc<ConnectCallback<T>>>, pub(super) on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>, _phantom: PhantomData<B>,
} }
impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
S::Error: Into<Error>,
X: Service<Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, h1::Codec>)>,
U::Error: Into<Error>,
{
pub(super) fn new(
cfg: ServiceConfig,
service: S,
expect: X,
upgrade: Option<U>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> HttpServiceHandler<T, S, B, X, U> {
HttpServiceHandler {
cfg,
on_connect_ext,
flow: HttpFlow::new(service, expect, upgrade),
_phantom: PhantomData,
}
}
pub(super) fn _poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?;
ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?;
if let Some(ref upg) = self.flow.upgrade {
ready!(upg.poll_ready(cx).map_err(Into::into))?;
};
Poll::Ready(Ok(()))
}
}
/// A collection of services that describe an HTTP request flow. /// A collection of services that describe an HTTP request flow.
pub(super) struct HttpFlow<S, X, U> { pub(super) struct HttpFlow<S, X, U> {
pub(super) service: S, pub(super) service: S,
@ -462,34 +461,6 @@ impl<S, X, U> HttpFlow<S, X, U> {
} }
} }
impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
X: Service<Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
{
fn new(
cfg: ServiceConfig,
service: S,
expect: X,
upgrade: Option<U>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> HttpServiceHandler<T, S, B, X, U> {
HttpServiceHandler {
cfg,
on_connect_ext,
flow: HttpFlow::new(service, expect, upgrade),
_phantom: PhantomData,
}
}
}
impl<T, S, B, X, U> Service<(T, Protocol, Option<net::SocketAddr>)> impl<T, S, B, X, U> Service<(T, Protocol, Option<net::SocketAddr>)>
for HttpServiceHandler<T, S, B, X, U> for HttpServiceHandler<T, S, B, X, U>
where where
@ -509,47 +480,10 @@ where
type Future = HttpServiceHandlerResponse<T, S, B, X, U>; type Future = HttpServiceHandlerResponse<T, S, B, X, U>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let ready = self self._poll_ready(cx).map_err(|e| {
.flow log::error!("HTTP service readiness error: {:?}", e);
.expect DispatchError::Service(e)
.poll_ready(cx) })
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready();
let ready = self
.flow
.service
.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready;
let ready = if let Some(ref upg) = self.flow.upgrade {
upg.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready
} else {
ready
};
if ready {
Poll::Ready(Ok(()))
} else {
Poll::Pending
}
} }
fn call( fn call(
@ -562,7 +496,7 @@ where
match proto { match proto {
Protocol::Http2 => HttpServiceHandlerResponse { Protocol::Http2 => HttpServiceHandlerResponse {
state: State::H2Handshake(Some(( state: State::H2Handshake(Some((
server::handshake(io), handshake(io),
self.cfg.clone(), self.cfg.clone(),
self.flow.clone(), self.flow.clone(),
on_connect_data, on_connect_data,

View File

@ -13,11 +13,6 @@ use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::{Method, Uri, Version}; use http::{Method, Uri, Version};
#[cfg(feature = "cookies")]
use crate::{
cookie::{Cookie, CookieJar},
header::{self, HeaderValue},
};
use crate::{ use crate::{
header::{HeaderMap, IntoHeaderPair}, header::{HeaderMap, IntoHeaderPair},
payload::Payload, payload::Payload,
@ -26,7 +21,7 @@ use crate::{
/// Test `Request` builder /// Test `Request` builder
/// ///
/// ```rust,ignore /// ```ignore
/// # use http::{header, StatusCode}; /// # use http::{header, StatusCode};
/// # use actix_web::*; /// # use actix_web::*;
/// use actix_web::test::TestRequest; /// use actix_web::test::TestRequest;
@ -54,8 +49,6 @@ struct Inner {
method: Method, method: Method,
uri: Uri, uri: Uri,
headers: HeaderMap, headers: HeaderMap,
#[cfg(feature = "cookies")]
cookies: CookieJar,
payload: Option<Payload>, payload: Option<Payload>,
} }
@ -66,8 +59,6 @@ impl Default for TestRequest {
uri: Uri::from_str("/").unwrap(), uri: Uri::from_str("/").unwrap(),
version: Version::HTTP_11, version: Version::HTTP_11,
headers: HeaderMap::new(), headers: HeaderMap::new(),
#[cfg(feature = "cookies")]
cookies: CookieJar::new(),
payload: None, payload: None,
})) }))
} }
@ -134,13 +125,6 @@ impl TestRequest {
self self
} }
/// Set cookie for this request.
#[cfg(feature = "cookies")]
pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self {
parts(&mut self.0).cookies.add(cookie.into_owned());
self
}
/// Set request payload. /// Set request payload.
pub fn set_payload<B: Into<Bytes>>(&mut self, data: B) -> &mut Self { pub fn set_payload<B: Into<Bytes>>(&mut self, data: B) -> &mut Self {
let mut payload = crate::h1::Payload::empty(); let mut payload = crate::h1::Payload::empty();
@ -169,22 +153,6 @@ impl TestRequest {
head.version = inner.version; head.version = inner.version;
head.headers = inner.headers; head.headers = inner.headers;
#[cfg(feature = "cookies")]
{
let cookie: String = inner
.cookies
.delta()
// ensure only name=value is written to cookie header
.map(|c| Cookie::new(c.name(), c.value()).encoded().to_string())
.collect::<Vec<_>>()
.join("; ");
if !cookie.is_empty() {
head.headers
.insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap());
}
}
req req
} }
} }

View File

@ -1,7 +1,7 @@
use time::{Date, OffsetDateTime, PrimitiveDateTime}; use time::{Date, OffsetDateTime, PrimitiveDateTime};
/// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime. /// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime.
pub fn parse_http_date(time: &str) -> Option<PrimitiveDateTime> { pub(crate) fn parse_http_date(time: &str) -> Option<PrimitiveDateTime> {
try_parse_rfc_1123(time) try_parse_rfc_1123(time)
.or_else(|| try_parse_rfc_850(time)) .or_else(|| try_parse_rfc_850(time))
.or_else(|| try_parse_asctime(time)) .or_else(|| try_parse_asctime(time))

View File

@ -4,7 +4,6 @@ use std::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};
use actix_utils::dispatcher::{Dispatcher as InnerDispatcher, DispatcherError};
use super::{Codec, Frame, Message}; use super::{Codec, Frame, Message};
@ -15,7 +14,7 @@ where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite,
{ {
#[pin] #[pin]
inner: InnerDispatcher<S, T, Codec, Message>, inner: inner::Dispatcher<S, T, Codec, Message>,
} }
impl<S, T> Dispatcher<S, T> impl<S, T> Dispatcher<S, T>
@ -27,13 +26,13 @@ where
{ {
pub fn new<F: IntoService<S, Frame>>(io: T, service: F) -> Self { pub fn new<F: IntoService<S, Frame>>(io: T, service: F) -> Self {
Dispatcher { Dispatcher {
inner: InnerDispatcher::new(Framed::new(io, Codec::new()), service), inner: inner::Dispatcher::new(Framed::new(io, Codec::new()), service),
} }
} }
pub fn with<F: IntoService<S, Frame>>(framed: Framed<T, Codec>, service: F) -> Self { pub fn with<F: IntoService<S, Frame>>(framed: Framed<T, Codec>, service: F) -> Self {
Dispatcher { Dispatcher {
inner: InnerDispatcher::new(framed, service), inner: inner::Dispatcher::new(framed, service),
} }
} }
} }
@ -45,9 +44,393 @@ where
S::Future: 'static, S::Future: 'static,
S::Error: 'static, S::Error: 'static,
{ {
type Output = Result<(), DispatcherError<S::Error, Codec, Message>>; type Output = Result<(), inner::DispatcherError<S::Error, Codec, Message>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project().inner.poll(cx) self.project().inner.poll(cx)
} }
} }
/// Framed dispatcher service and related utilities.
mod inner {
// allow dead code since this mod was ripped from actix-utils
#![allow(dead_code)]
use core::{
fmt,
future::Future,
mem,
pin::Pin,
task::{Context, Poll},
};
use actix_service::{IntoService, Service};
use futures_core::stream::Stream;
use local_channel::mpsc;
use log::debug;
use pin_project_lite::pin_project;
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
use crate::ResponseError;
/// Framed transport errors
pub enum DispatcherError<E, U, I>
where
U: Encoder<I> + Decoder,
{
/// Inner service error.
Service(E),
/// Frame encoding error.
Encoder(<U as Encoder<I>>::Error),
/// Frame decoding error.
Decoder(<U as Decoder>::Error),
}
impl<E, U, I> From<E> for DispatcherError<E, U, I>
where
U: Encoder<I> + Decoder,
{
fn from(err: E) -> Self {
DispatcherError::Service(err)
}
}
impl<E, U, I> fmt::Debug for DispatcherError<E, U, I>
where
E: fmt::Debug,
U: Encoder<I> + Decoder,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
DispatcherError::Service(ref e) => {
write!(fmt, "DispatcherError::Service({:?})", e)
}
DispatcherError::Encoder(ref e) => {
write!(fmt, "DispatcherError::Encoder({:?})", e)
}
DispatcherError::Decoder(ref e) => {
write!(fmt, "DispatcherError::Decoder({:?})", e)
}
}
}
}
impl<E, U, I> fmt::Display for DispatcherError<E, U, I>
where
E: fmt::Display,
U: Encoder<I> + Decoder,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
DispatcherError::Service(ref e) => write!(fmt, "{}", e),
DispatcherError::Encoder(ref e) => write!(fmt, "{:?}", e),
DispatcherError::Decoder(ref e) => write!(fmt, "{:?}", e),
}
}
}
impl<E, U, I> ResponseError for DispatcherError<E, U, I>
where
E: fmt::Debug + fmt::Display,
U: Encoder<I> + Decoder,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
}
/// Message type wrapper for signalling end of message stream.
pub enum Message<T> {
/// Message item.
Item(T),
/// Signal from service to flush all messages and stop processing.
Close,
}
pin_project! {
/// A future that reads frames from a [`Framed`] object and passes them to a [`Service`].
pub struct Dispatcher<S, T, U, I>
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead,
T: AsyncWrite,
U: Encoder<I>,
U: Decoder,
I: 'static,
<U as Encoder<I>>::Error: fmt::Debug,
{
service: S,
state: State<S, U, I>,
#[pin]
framed: Framed<T, U>,
rx: mpsc::Receiver<Result<Message<I>, S::Error>>,
tx: mpsc::Sender<Result<Message<I>, S::Error>>,
}
}
enum State<S, U, I>
where
S: Service<<U as Decoder>::Item>,
U: Encoder<I> + Decoder,
{
Processing,
Error(DispatcherError<S::Error, U, I>),
FramedError(DispatcherError<S::Error, U, I>),
FlushAndStop,
Stopping,
}
impl<S, U, I> State<S, U, I>
where
S: Service<<U as Decoder>::Item>,
U: Encoder<I> + Decoder,
{
fn take_error(&mut self) -> DispatcherError<S::Error, U, I> {
match mem::replace(self, State::Processing) {
State::Error(err) => err,
_ => panic!(),
}
}
fn take_framed_error(&mut self) -> DispatcherError<S::Error, U, I> {
match mem::replace(self, State::Processing) {
State::FramedError(err) => err,
_ => panic!(),
}
}
}
impl<S, T, U, I> Dispatcher<S, T, U, I>
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead + AsyncWrite,
U: Decoder + Encoder<I>,
I: 'static,
<U as Decoder>::Error: fmt::Debug,
<U as Encoder<I>>::Error: fmt::Debug,
{
/// Create new `Dispatcher`.
pub fn new<F>(framed: Framed<T, U>, service: F) -> Self
where
F: IntoService<S, <U as Decoder>::Item>,
{
let (tx, rx) = mpsc::channel();
Dispatcher {
framed,
rx,
tx,
service: service.into_service(),
state: State::Processing,
}
}
/// Construct new `Dispatcher` instance with customer `mpsc::Receiver`
pub fn with_rx<F>(
framed: Framed<T, U>,
service: F,
rx: mpsc::Receiver<Result<Message<I>, S::Error>>,
) -> Self
where
F: IntoService<S, <U as Decoder>::Item>,
{
let tx = rx.sender();
Dispatcher {
framed,
rx,
tx,
service: service.into_service(),
state: State::Processing,
}
}
/// Get sender handle.
pub fn tx(&self) -> mpsc::Sender<Result<Message<I>, S::Error>> {
self.tx.clone()
}
/// Get reference to a service wrapped by `Dispatcher` instance.
pub fn service(&self) -> &S {
&self.service
}
/// Get mutable reference to a service wrapped by `Dispatcher` instance.
pub fn service_mut(&mut self) -> &mut S {
&mut self.service
}
/// Get reference to a framed instance wrapped by `Dispatcher` instance.
pub fn framed(&self) -> &Framed<T, U> {
&self.framed
}
/// Get mutable reference to a framed instance wrapped by `Dispatcher` instance.
pub fn framed_mut(&mut self) -> &mut Framed<T, U> {
&mut self.framed
}
/// Read from framed object.
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> bool
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead + AsyncWrite,
U: Decoder + Encoder<I>,
I: 'static,
<U as Encoder<I>>::Error: fmt::Debug,
{
loop {
let this = self.as_mut().project();
match this.service.poll_ready(cx) {
Poll::Ready(Ok(_)) => {
let item = match this.framed.next_item(cx) {
Poll::Ready(Some(Ok(el))) => el,
Poll::Ready(Some(Err(err))) => {
*this.state =
State::FramedError(DispatcherError::Decoder(err));
return true;
}
Poll::Pending => return false,
Poll::Ready(None) => {
*this.state = State::Stopping;
return true;
}
};
let tx = this.tx.clone();
let fut = this.service.call(item);
actix_rt::spawn(async move {
let item = fut.await;
let _ = tx.send(item.map(Message::Item));
});
}
Poll::Pending => return false,
Poll::Ready(Err(err)) => {
*this.state = State::Error(DispatcherError::Service(err));
return true;
}
}
}
}
/// Write to framed object.
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> bool
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead + AsyncWrite,
U: Decoder + Encoder<I>,
I: 'static,
<U as Encoder<I>>::Error: fmt::Debug,
{
loop {
let mut this = self.as_mut().project();
while !this.framed.is_write_buf_full() {
match Pin::new(&mut this.rx).poll_next(cx) {
Poll::Ready(Some(Ok(Message::Item(msg)))) => {
if let Err(err) = this.framed.as_mut().write(msg) {
*this.state =
State::FramedError(DispatcherError::Encoder(err));
return true;
}
}
Poll::Ready(Some(Ok(Message::Close))) => {
*this.state = State::FlushAndStop;
return true;
}
Poll::Ready(Some(Err(err))) => {
*this.state = State::Error(DispatcherError::Service(err));
return true;
}
Poll::Ready(None) | Poll::Pending => break,
}
}
if !this.framed.is_write_buf_empty() {
match this.framed.flush(cx) {
Poll::Pending => break,
Poll::Ready(Ok(_)) => {}
Poll::Ready(Err(err)) => {
debug!("Error sending data: {:?}", err);
*this.state =
State::FramedError(DispatcherError::Encoder(err));
return true;
}
}
} else {
break;
}
}
false
}
}
impl<S, T, U, I> Future for Dispatcher<S, T, U, I>
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead + AsyncWrite,
U: Decoder + Encoder<I>,
I: 'static,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
type Output = Result<(), DispatcherError<S::Error, U, I>>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
loop {
let this = self.as_mut().project();
return match this.state {
State::Processing => {
if self.as_mut().poll_read(cx) || self.as_mut().poll_write(cx) {
continue;
} else {
Poll::Pending
}
}
State::Error(_) => {
// flush write buffer
if !this.framed.is_write_buf_empty()
&& this.framed.flush(cx).is_pending()
{
return Poll::Pending;
}
Poll::Ready(Err(this.state.take_error()))
}
State::FlushAndStop => {
if !this.framed.is_write_buf_empty() {
this.framed.flush(cx).map(|res| {
if let Err(err) = res {
debug!("Error sending data: {:?}", err);
}
Ok(())
})
} else {
Poll::Ready(Ok(()))
}
}
State::FramedError(_) => {
Poll::Ready(Err(this.state.take_framed_error()))
}
State::Stopping => Poll::Ready(Ok(())),
};
}
}
}
}

View File

@ -9,10 +9,8 @@ use derive_more::{Display, Error, From};
use http::{header, Method, StatusCode}; use http::{header, Method, StatusCode};
use crate::{ use crate::{
error::ResponseError, body::Body, error::ResponseError, header::HeaderValue, message::RequestHead,
header::HeaderValue, response::Response, ResponseBuilder,
message::RequestHead,
response::{Response, ResponseBuilder},
}; };
mod codec; mod codec;
@ -101,31 +99,39 @@ pub enum HandshakeError {
} }
impl ResponseError for HandshakeError { impl ResponseError for HandshakeError {
fn error_response(&self) -> Response { fn error_response(&self) -> Response<Body> {
match self { match self {
HandshakeError::GetMethodRequired => Response::MethodNotAllowed() HandshakeError::GetMethodRequired => {
.insert_header((header::ALLOW, "GET")) Response::build(StatusCode::METHOD_NOT_ALLOWED)
.finish(), .insert_header((header::ALLOW, "GET"))
.finish()
}
HandshakeError::NoWebsocketUpgrade => Response::BadRequest() HandshakeError::NoWebsocketUpgrade => {
.reason("No WebSocket Upgrade header found") Response::build(StatusCode::BAD_REQUEST)
.finish(), .reason("No WebSocket Upgrade header found")
.finish()
}
HandshakeError::NoConnectionUpgrade => Response::BadRequest() HandshakeError::NoConnectionUpgrade => {
.reason("No Connection upgrade") Response::build(StatusCode::BAD_REQUEST)
.finish(), .reason("No Connection upgrade")
.finish()
}
HandshakeError::NoVersionHeader => Response::BadRequest() HandshakeError::NoVersionHeader => Response::build(StatusCode::BAD_REQUEST)
.reason("WebSocket version header is required") .reason("WebSocket version header is required")
.finish(), .finish(),
HandshakeError::UnsupportedVersion => Response::BadRequest() HandshakeError::UnsupportedVersion => {
.reason("Unsupported WebSocket version") Response::build(StatusCode::BAD_REQUEST)
.finish(), .reason("Unsupported WebSocket version")
.finish()
HandshakeError::BadWebsocketKey => {
Response::BadRequest().reason("Handshake error").finish()
} }
HandshakeError::BadWebsocketKey => Response::build(StatusCode::BAD_REQUEST)
.reason("Handshake error")
.finish(),
} }
} }
} }
@ -322,17 +328,17 @@ mod tests {
#[test] #[test]
fn test_wserror_http_response() { fn test_wserror_http_response() {
let resp: Response = HandshakeError::GetMethodRequired.error_response(); let resp = HandshakeError::GetMethodRequired.error_response();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let resp: Response = HandshakeError::NoWebsocketUpgrade.error_response(); let resp = HandshakeError::NoWebsocketUpgrade.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response = HandshakeError::NoConnectionUpgrade.error_response(); let resp = HandshakeError::NoConnectionUpgrade.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response = HandshakeError::NoVersionHeader.error_response(); let resp = HandshakeError::NoVersionHeader.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response = HandshakeError::UnsupportedVersion.error_response(); let resp = HandshakeError::UnsupportedVersion.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response = HandshakeError::BadWebsocketKey.error_response(); let resp = HandshakeError::BadWebsocketKey.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
} }
} }

View File

@ -3,11 +3,9 @@ use actix_http::{
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_utils::future;
use bytes::Bytes; use bytes::Bytes;
use futures_util::{ use futures_util::StreamExt as _;
future::{self, ok},
StreamExt,
};
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ 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 \
@ -35,7 +33,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h1_v2() { async fn test_h1_v2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .finish(|_| future::ok::<_, ()>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -63,7 +61,7 @@ async fn test_h1_v2() {
async fn test_connection_close() { async fn test_connection_close() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .finish(|_| future::ok::<_, ()>(Response::ok().set_body(STR)))
.tcp() .tcp()
.map(|_| ()) .map(|_| ())
}) })
@ -79,9 +77,9 @@ async fn test_with_query_parameter() {
HttpService::build() HttpService::build()
.finish(|req: Request| { .finish(|req: Request| {
if req.uri().query().unwrap().contains("qp=") { if req.uri().query().unwrap().contains("qp=") {
ok::<_, ()>(Response::Ok().finish()) future::ok::<_, ()>(Response::ok())
} else { } else {
ok::<_, ()>(Response::BadRequest().finish()) future::ok::<_, ()>(Response::bad_request())
} }
}) })
.tcp() .tcp()
@ -114,7 +112,7 @@ async fn test_h1_expect() {
let str = std::str::from_utf8(&buf).unwrap(); let str = std::str::from_utf8(&buf).unwrap();
assert_eq!(str, "expect body"); assert_eq!(str, "expect body");
Ok::<_, ()>(Response::Ok().finish()) Ok::<_, ()>(Response::ok())
}) })
.tcp() .tcp()
}) })

View File

@ -4,16 +4,21 @@ extern crate tls_openssl as openssl;
use std::io; use std::io;
use actix_http::error::{ErrorBadRequest, PayloadError}; use actix_http::{
use actix_http::http::header::{self, HeaderName, HeaderValue}; body::{Body, SizedStream},
use actix_http::http::{Method, StatusCode, Version}; error::{ErrorBadRequest, PayloadError},
use actix_http::HttpMessage; http::{
use actix_http::{body, Error, HttpService, Request, Response}; header::{self, HeaderName, HeaderValue},
Method, StatusCode, Version,
},
Error, HttpMessage, HttpService, Request, Response,
};
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_service, ServiceFactoryExt}; use actix_service::{fn_service, ServiceFactoryExt};
use actix_utils::future::{err, ok, ready};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_util::future::{err, ok, ready}; use futures_core::Stream;
use futures_util::stream::{once, Stream, StreamExt}; use futures_util::stream::{once, StreamExt as _};
use openssl::{ use openssl::{
pkey::PKey, pkey::PKey,
ssl::{SslAcceptor, SslMethod}, ssl::{SslAcceptor, SslMethod},
@ -66,7 +71,7 @@ fn tls_config() -> SslAcceptor {
async fn test_h2() -> io::Result<()> { async fn test_h2() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, Error>(Response::Ok().finish())) .h2(|_| ok::<_, Error>(Response::ok()))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -84,7 +89,7 @@ async fn test_h2_1() -> io::Result<()> {
.finish(|req: Request| { .finish(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
assert_eq!(req.version(), Version::HTTP_2); assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::Ok().finish()) ok::<_, Error>(Response::ok())
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
@ -103,7 +108,7 @@ async fn test_h2_body() -> io::Result<()> {
HttpService::build() HttpService::build()
.h2(|mut req: Request<_>| async move { .h2(|mut req: Request<_>| async move {
let body = load_body(req.take_payload()).await?; let body = load_body(req.take_payload()).await?;
Ok::<_, Error>(Response::Ok().body(body)) Ok::<_, Error>(Response::ok().set_body(body))
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
@ -181,7 +186,7 @@ async fn test_h2_headers() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
let data = data.clone(); let data = data.clone();
HttpService::build().h2(move |_| { HttpService::build().h2(move |_| {
let mut builder = Response::Ok(); let mut builder = Response::build(StatusCode::OK);
for idx in 0..90 { for idx in 0..90 {
builder.insert_header( builder.insert_header(
(format!("X-TEST-{}", idx).as_str(), (format!("X-TEST-{}", idx).as_str(),
@ -240,7 +245,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h2_body2() { async fn test_h2_body2() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -258,7 +263,7 @@ async fn test_h2_body2() {
async fn test_h2_head_empty() { async fn test_h2_head_empty() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .finish(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -282,7 +287,7 @@ async fn test_h2_head_empty() {
async fn test_h2_head_binary() { async fn test_h2_head_binary() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -305,7 +310,7 @@ async fn test_h2_head_binary() {
async fn test_h2_head_binary2() { async fn test_h2_head_binary2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -327,7 +332,7 @@ async fn test_h2_body_length() {
.h2(|_| { .h2(|_| {
let body = once(ok(Bytes::from_static(STR.as_ref()))); let body = once(ok(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, ()>(
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
.openssl(tls_config()) .openssl(tls_config())
@ -350,7 +355,7 @@ async fn test_h2_body_chunked_explicit() {
.h2(|_| { .h2(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, ()>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .streaming(body),
) )
@ -378,7 +383,7 @@ async fn test_h2_response_http_error_handling() {
.h2(fn_service(|_| { .h2(fn_service(|_| {
let broken_header = Bytes::from_static(b"\0\0\0"); let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>( ok::<_, ()>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((header::CONTENT_TYPE, broken_header)) .insert_header((header::CONTENT_TYPE, broken_header))
.body(STR), .body(STR),
) )
@ -400,7 +405,7 @@ async fn test_h2_response_http_error_handling() {
async fn test_h2_service_error() { async fn test_h2_service_error() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| err::<Response, Error>(ErrorBadRequest("error"))) .h2(|_| err::<Response<Body>, Error>(ErrorBadRequest("error")))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -423,7 +428,7 @@ async fn test_h2_on_connect() {
}) })
.h2(|req: Request| { .h2(|req: Request| {
assert!(req.extensions().contains::<isize>()); assert!(req.extensions().contains::<isize>());
ok::<_, ()>(Response::Ok().finish()) ok::<_, ()>(Response::ok())
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())

View File

@ -2,16 +2,22 @@
extern crate tls_rustls as rustls; extern crate tls_rustls as rustls;
use actix_http::error::PayloadError; use actix_http::{
use actix_http::http::header::{self, HeaderName, HeaderValue}; body::{Body, SizedStream},
use actix_http::http::{Method, StatusCode, Version}; error::{self, PayloadError},
use actix_http::{body, error, Error, HttpService, Request, Response}; http::{
header::{self, HeaderName, HeaderValue},
Method, StatusCode, Version,
},
Error, HttpService, Request, Response,
};
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_factory_with_config, fn_service}; use actix_service::{fn_factory_with_config, fn_service};
use actix_utils::future::{err, ok};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_util::future::{self, err, ok}; use futures_core::Stream;
use futures_util::stream::{once, Stream, StreamExt}; use futures_util::stream::{once, StreamExt as _};
use rustls::{ use rustls::{
internal::pemfile::{certs, pkcs8_private_keys}, internal::pemfile::{certs, pkcs8_private_keys},
NoClientAuth, ServerConfig as RustlsServerConfig, NoClientAuth, ServerConfig as RustlsServerConfig,
@ -50,7 +56,7 @@ fn tls_config() -> RustlsServerConfig {
async fn test_h1() -> io::Result<()> { async fn test_h1() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, Error>(Response::Ok().finish())) .h1(|_| ok::<_, Error>(Response::ok()))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -64,7 +70,7 @@ async fn test_h1() -> io::Result<()> {
async fn test_h2() -> io::Result<()> { async fn test_h2() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| future::ok::<_, Error>(Response::Ok().finish())) .h2(|_| ok::<_, Error>(Response::ok()))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -81,7 +87,7 @@ async fn test_h1_1() -> io::Result<()> {
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
assert_eq!(req.version(), Version::HTTP_11); assert_eq!(req.version(), Version::HTTP_11);
future::ok::<_, Error>(Response::Ok().finish()) ok::<_, Error>(Response::ok())
}) })
.rustls(tls_config()) .rustls(tls_config())
}) })
@ -99,7 +105,7 @@ async fn test_h2_1() -> io::Result<()> {
.finish(|req: Request| { .finish(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
assert_eq!(req.version(), Version::HTTP_2); assert_eq!(req.version(), Version::HTTP_2);
future::ok::<_, Error>(Response::Ok().finish()) ok::<_, Error>(Response::ok())
}) })
.rustls(tls_config()) .rustls(tls_config())
}) })
@ -117,7 +123,7 @@ async fn test_h2_body1() -> io::Result<()> {
HttpService::build() HttpService::build()
.h2(|mut req: Request<_>| async move { .h2(|mut req: Request<_>| async move {
let body = load_body(req.take_payload()).await?; let body = load_body(req.take_payload()).await?;
Ok::<_, Error>(Response::Ok().body(body)) Ok::<_, Error>(Response::ok().set_body(body))
}) })
.rustls(tls_config()) .rustls(tls_config())
}) })
@ -143,7 +149,7 @@ async fn test_h2_content_length() {
StatusCode::OK, StatusCode::OK,
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
]; ];
future::ok::<_, ()>(Response::new(statuses[indx])) ok::<_, ()>(Response::new(statuses[indx]))
}) })
.rustls(tls_config()) .rustls(tls_config())
}) })
@ -193,7 +199,7 @@ async fn test_h2_headers() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
let data = data.clone(); let data = data.clone();
HttpService::build().h2(move |_| { HttpService::build().h2(move |_| {
let mut config = Response::Ok(); let mut config = Response::build(StatusCode::OK);
for idx in 0..90 { for idx in 0..90 {
config.insert_header(( config.insert_header((
format!("X-TEST-{}", idx).as_str(), format!("X-TEST-{}", idx).as_str(),
@ -212,7 +218,7 @@ async fn test_h2_headers() {
TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ",
)); ));
} }
future::ok::<_, ()>(config.body(data.clone())) ok::<_, ()>(config.body(data.clone()))
}) })
.rustls(tls_config()) .rustls(tls_config())
}).await; }).await;
@ -251,7 +257,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h2_body2() { async fn test_h2_body2() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -268,7 +274,7 @@ async fn test_h2_body2() {
async fn test_h2_head_empty() { async fn test_h2_head_empty() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .finish(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -294,7 +300,7 @@ async fn test_h2_head_empty() {
async fn test_h2_head_binary() { async fn test_h2_head_binary() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -319,7 +325,7 @@ async fn test_h2_head_binary() {
async fn test_h2_head_binary2() { async fn test_h2_head_binary2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -343,7 +349,7 @@ async fn test_h2_body_length() {
.h2(|_| { .h2(|_| {
let body = once(ok(Bytes::from_static(STR.as_ref()))); let body = once(ok(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, ()>(
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
.rustls(tls_config()) .rustls(tls_config())
@ -365,7 +371,7 @@ async fn test_h2_body_chunked_explicit() {
.h2(|_| { .h2(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, ()>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .streaming(body),
) )
@ -393,7 +399,7 @@ async fn test_h2_response_http_error_handling() {
ok::<_, ()>(fn_service(|_| { ok::<_, ()>(fn_service(|_| {
let broken_header = Bytes::from_static(b"\0\0\0"); let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>( ok::<_, ()>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((http::header::CONTENT_TYPE, broken_header)) .insert_header((http::header::CONTENT_TYPE, broken_header))
.body(STR), .body(STR),
) )
@ -415,7 +421,7 @@ async fn test_h2_response_http_error_handling() {
async fn test_h2_service_error() { async fn test_h2_service_error() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| err::<Response, Error>(error::ErrorBadRequest("error"))) .h2(|_| err::<Response<Body>, Error>(error::ErrorBadRequest("error")))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -432,7 +438,7 @@ async fn test_h2_service_error() {
async fn test_h1_service_error() { async fn test_h1_service_error() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h1(|_| err::<Response, Error>(error::ErrorBadRequest("error"))) .h1(|_| err::<Response<Body>, Error>(error::ErrorBadRequest("error")))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;

View File

@ -5,14 +5,18 @@ use std::{net, thread};
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_rt::time::sleep; use actix_rt::time::sleep;
use actix_service::fn_service; use actix_service::fn_service;
use actix_utils::future::{err, ok, ready};
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::{self, err, ok, ready, FutureExt}; use futures_util::stream::{once, StreamExt as _};
use futures_util::stream::{once, StreamExt}; use futures_util::FutureExt as _;
use regex::Regex; use regex::Regex;
use actix_http::HttpMessage; use actix_http::HttpMessage;
use actix_http::{ use actix_http::{
body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response, body::{Body, SizedStream},
error,
http::{self, header, StatusCode},
Error, HttpService, KeepAlive, Request, Response,
}; };
#[actix_rt::test] #[actix_rt::test]
@ -24,7 +28,7 @@ async fn test_h1() {
.client_disconnect(1000) .client_disconnect(1000)
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
future::ok::<_, ()>(Response::Ok().finish()) ok::<_, ()>(Response::ok())
}) })
.tcp() .tcp()
}) })
@ -44,7 +48,7 @@ async fn test_h1_2() {
.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);
future::ok::<_, ()>(Response::Ok().finish()) ok::<_, ()>(Response::ok())
}) })
.tcp() .tcp()
}) })
@ -65,7 +69,7 @@ async fn test_expect_continue() {
err(error::ErrorPreconditionFailed("error")) err(error::ErrorPreconditionFailed("error"))
} }
})) }))
.finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .finish(|_| ok::<_, ()>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -96,7 +100,7 @@ async fn test_expect_continue_h1() {
} }
}) })
})) }))
.h1(fn_service(|_| future::ok::<_, ()>(Response::Ok().finish()))) .h1(fn_service(|_| ok::<_, ()>(Response::ok())))
.tcp() .tcp()
}) })
.await; .await;
@ -130,7 +134,9 @@ async fn test_chunked_payload() {
}) })
.fold(0usize, |acc, chunk| ready(acc + chunk.len())) .fold(0usize, |acc, chunk| ready(acc + chunk.len()))
.map(|req_size| { .map(|req_size| {
Ok::<_, Error>(Response::Ok().body(format!("size={}", req_size))) Ok::<_, Error>(
Response::ok().set_body(format!("size={}", req_size)),
)
}) })
})) }))
.tcp() .tcp()
@ -175,7 +181,7 @@ async fn test_slow_request() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.client_timeout(100) .client_timeout(100)
.finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .finish(|_| ok::<_, ()>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -191,7 +197,7 @@ async fn test_slow_request() {
async fn test_http1_malformed_request() { async fn test_http1_malformed_request() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, ()>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -207,7 +213,7 @@ async fn test_http1_malformed_request() {
async fn test_http1_keepalive() { async fn test_http1_keepalive() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, ()>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -229,7 +235,7 @@ async fn test_http1_keepalive_timeout() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(1) .keep_alive(1)
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, ()>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -250,7 +256,7 @@ async fn test_http1_keepalive_timeout() {
async fn test_http1_keepalive_close() { async fn test_http1_keepalive_close() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, ()>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -271,7 +277,7 @@ async fn test_http1_keepalive_close() {
async fn test_http10_keepalive_default_close() { async fn test_http10_keepalive_default_close() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, ()>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -291,7 +297,7 @@ async fn test_http10_keepalive_default_close() {
async fn test_http10_keepalive() { async fn test_http10_keepalive() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, ()>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -319,7 +325,7 @@ async fn test_http1_keepalive_disabled() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, ()>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -354,7 +360,7 @@ async fn test_content_length() {
StatusCode::OK, StatusCode::OK,
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
]; ];
future::ok::<_, ()>(Response::new(statuses[indx])) ok::<_, ()>(Response::new(statuses[indx]))
}) })
.tcp() .tcp()
}) })
@ -390,7 +396,7 @@ async fn test_h1_headers() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
let data = data.clone(); let data = data.clone();
HttpService::build().h1(move |_| { HttpService::build().h1(move |_| {
let mut builder = Response::Ok(); let mut builder = Response::build(StatusCode::OK);
for idx in 0..90 { for idx in 0..90 {
builder.insert_header(( builder.insert_header((
format!("X-TEST-{}", idx).as_str(), format!("X-TEST-{}", idx).as_str(),
@ -409,7 +415,7 @@ async fn test_h1_headers() {
TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ",
)); ));
} }
future::ok::<_, ()>(builder.body(data.clone())) ok::<_, ()>(builder.body(data.clone()))
}).tcp() }).tcp()
}).await; }).await;
@ -447,7 +453,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h1_body() { async fn test_h1_body() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .h1(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -464,7 +470,7 @@ async fn test_h1_body() {
async fn test_h1_head_empty() { async fn test_h1_head_empty() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .h1(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -489,7 +495,7 @@ async fn test_h1_head_empty() {
async fn test_h1_head_binary() { async fn test_h1_head_binary() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .h1(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -514,7 +520,7 @@ async fn test_h1_head_binary() {
async fn test_h1_head_binary2() { async fn test_h1_head_binary2() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .h1(|_| ok::<_, ()>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -538,7 +544,7 @@ async fn test_h1_body_length() {
.h1(|_| { .h1(|_| {
let body = once(ok(Bytes::from_static(STR.as_ref()))); let body = once(ok(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, ()>(
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
.tcp() .tcp()
@ -560,7 +566,7 @@ async fn test_h1_body_chunked_explicit() {
.h1(|_| { .h1(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, ()>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .streaming(body),
) )
@ -594,7 +600,7 @@ async fn test_h1_body_chunked_implicit() {
HttpService::build() HttpService::build()
.h1(|_| { .h1(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>(Response::Ok().streaming(body)) ok::<_, ()>(Response::build(StatusCode::OK).streaming(body))
}) })
.tcp() .tcp()
}) })
@ -624,7 +630,7 @@ async fn test_h1_response_http_error_handling() {
.h1(fn_service(|_| { .h1(fn_service(|_| {
let broken_header = Bytes::from_static(b"\0\0\0"); let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>( ok::<_, ()>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((http::header::CONTENT_TYPE, broken_header)) .insert_header((http::header::CONTENT_TYPE, broken_header))
.body(STR), .body(STR),
) )
@ -645,7 +651,7 @@ async fn test_h1_response_http_error_handling() {
async fn test_h1_service_error() { async fn test_h1_service_error() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::err::<Response, Error>(error::ErrorBadRequest("error"))) .h1(|_| err::<Response<Body>, _>(error::ErrorBadRequest("error")))
.tcp() .tcp()
}) })
.await; .await;
@ -667,7 +673,7 @@ async fn test_h1_on_connect() {
}) })
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.extensions().contains::<isize>()); assert!(req.extensions().contains::<isize>());
future::ok::<_, ()>(Response::Ok().finish()) ok::<_, ()>(Response::ok())
}) })
.tcp() .tcp()
}) })

View File

@ -3,16 +3,17 @@ use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin; use std::pin::Pin;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_http::{body, h1, ws, Error, HttpService, Request, Response}; use actix_http::{body, h1, ws, Error, HttpService, Request, Response};
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_factory, Service}; use actix_service::{fn_factory, Service};
use actix_utils::dispatcher::Dispatcher; use actix_utils::future;
use bytes::Bytes; use bytes::Bytes;
use futures_util::future; use futures_util::{SinkExt as _, StreamExt as _};
use futures_util::task::{Context, Poll};
use futures_util::{SinkExt, StreamExt}; use crate::ws::Dispatcher;
struct WsService<T>(Arc<Mutex<(PhantomData<T>, Cell<bool>)>>); struct WsService<T>(Arc<Mutex<(PhantomData<T>, Cell<bool>)>>);
@ -58,7 +59,7 @@ where
.await .await
.unwrap(); .unwrap();
Dispatcher::new(framed.replace_codec(ws::Codec::new()), service) Dispatcher::with(framed.replace_codec(ws::Codec::new()), service)
.await .await
.map_err(|_| panic!()) .map_err(|_| panic!())
}; };
@ -90,7 +91,7 @@ async fn test_simple() {
let ws_service = ws_service.clone(); let ws_service = ws_service.clone();
HttpService::build() HttpService::build()
.upgrade(fn_factory(move || future::ok::<_, ()>(ws_service.clone()))) .upgrade(fn_factory(move || future::ok::<_, ()>(ws_service.clone())))
.finish(|_| future::ok::<_, ()>(Response::NotFound())) .finish(|_| future::ok::<_, ()>(Response::not_found()))
.tcp() .tcp()
} }
}) })

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.4.0-beta.4 - 2021-04-02
* No notable changes.
## 0.4.0-beta.3 - 2021-03-09 ## 0.4.0-beta.3 - 2021-03-09
* No notable changes. * No notable changes.

View File

@ -1,13 +1,13 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.3" version = "0.4.0-beta.4"
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"
readme = "README.md" readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-multipart/" documentation = "https://docs.rs/actix-multipart"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
@ -16,19 +16,21 @@ name = "actix_multipart"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.4", default-features = false } actix-web = { version = "4.0.0-beta.6", default-features = false }
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0"
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
httparse = "1.3" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
httparse = "1.3"
local-waker = "0.1"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
twoway = "0.2" twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.1" actix-rt = "2.2"
actix-http = "3.0.0-beta.4" actix-http = "3.0.0-beta.6"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", 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.3)](https://docs.rs/actix-multipart/0.4.0-beta.3) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.4)](https://docs.rs/actix-multipart/0.4.0-beta.4)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.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.3/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.3) [![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.4/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.4)
[![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)
## Documentation & Resources ## Documentation & Resources

View File

@ -45,11 +45,10 @@ impl ResponseError for MultipartError {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use actix_web::HttpResponse;
#[test] #[test]
fn test_multipart_error() { fn test_multipart_error() {
let resp: HttpResponse = MultipartError::Boundary.error_response(); let resp = MultipartError::Boundary.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
} }
} }

View File

@ -1,21 +1,22 @@
//! Multipart payload support //! Multipart payload support
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest}; use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
use futures_util::future::{ok, Ready};
use crate::server::Multipart; use crate::server::Multipart;
/// Get request's payload as multipart stream /// Get request's payload as multipart stream.
/// ///
/// Content-type: multipart/form-data; /// Content-type: multipart/form-data;
/// ///
/// ## Server example /// ## Server example
/// ///
/// ```rust /// ```
/// use futures_util::stream::{Stream, StreamExt};
/// use actix_web::{web, HttpResponse, Error}; /// use actix_web::{web, HttpResponse, Error};
/// use actix_multipart as mp; /// use actix_multipart::Multipart;
/// use futures_util::stream::StreamExt as _;
/// ///
/// async fn index(mut payload: mp::Multipart) -> Result<HttpResponse, Error> { /// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> {
/// // iterate over multipart stream /// // iterate over multipart stream
/// while let Some(item) = payload.next().await { /// while let Some(item) = payload.next().await {
/// let mut field = item?; /// let mut field = item?;
@ -25,9 +26,9 @@ use crate::server::Multipart;
/// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk?)); /// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk?));
/// } /// }
/// } /// }
///
/// Ok(HttpResponse::Ok().into()) /// Ok(HttpResponse::Ok().into())
/// } /// }
/// # fn main() {}
/// ``` /// ```
impl FromRequest for Multipart { impl FromRequest for Multipart {
type Error = Error; type Error = Error;
@ -36,9 +37,9 @@ impl FromRequest for Multipart {
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
ok(match Multipart::boundary(req.headers()) { ready(Ok(match Multipart::boundary(req.headers()) {
Ok(boundary) => Multipart::from_boundary(boundary, payload.take()), Ok(boundary) => Multipart::from_boundary(boundary, payload.take()),
Err(err) => Multipart::from_error(err), Err(err) => Multipart::from_error(err),
}) }))
} }
} }

View File

@ -1,4 +1,4 @@
//! Multipart payload support //! Multipart response payload support.
use std::cell::{Cell, RefCell, RefMut}; use std::cell::{Cell, RefCell, RefMut};
use std::convert::TryFrom; use std::convert::TryFrom;
@ -8,12 +8,12 @@ use std::rc::Rc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::{cmp, fmt}; use std::{cmp, fmt};
use bytes::{Bytes, BytesMut};
use futures_util::stream::{LocalBoxStream, Stream, StreamExt};
use actix_utils::task::LocalWaker;
use actix_web::error::{ParseError, PayloadError}; use actix_web::error::{ParseError, PayloadError};
use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue};
use bytes::{Bytes, BytesMut};
use futures_core::stream::{LocalBoxStream, Stream};
use futures_util::stream::StreamExt as _;
use local_waker::LocalWaker;
use crate::error::MultipartError; use crate::error::MultipartError;

9
actix-test/CHANGES.md Normal file
View File

@ -0,0 +1,9 @@
# Changes
## Unreleased - 2021-xx-xx
## 0.1.0-beta.1 - 2021-04-02
* Move integration testing structs from `actix-web`. [#2112]
[#2112]: https://github.com/actix/actix-web/pull/2112

38
actix-test/Cargo.toml Normal file
View File

@ -0,0 +1,38 @@
[package]
name = "actix-test"
version = "0.1.0-beta.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
edition = "2018"
description = "Integration testing tools for Actix Web applications"
license = "MIT OR Apache-2.0"
[features]
default = []
# rustls
rustls = ["tls-rustls", "actix-http/rustls"]
# openssl
openssl = ["tls-openssl", "actix-http/openssl"]
[dependencies]
actix-codec = "0.4.0-beta.1"
actix-http = "3.0.0-beta.6"
actix-http-test = { version = "3.0.0-beta.4", features = [] }
actix-service = "2.0.0"
actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.6", default-features = false, features = ["cookies"] }
actix-rt = "2.1"
awc = { version = "3.0.0-beta.5", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] }
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_urlencoded = "0.7"
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.19.0", optional = true }

1
actix-test/LICENSE-APACHE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-APACHE

1
actix-test/LICENSE-MIT Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-MIT

472
actix-test/src/lib.rs Normal file
View File

@ -0,0 +1,472 @@
//! Integration testing tools for Actix Web applications.
//!
//! The main integration testing tool is [`TestServer`]. It spawns a real HTTP server on an
//! unused port and provides methods that use a real HTTP client. Therefore, it is much closer to
//! real-world cases than using `init_service`, which skips HTTP encoding and decoding.
//!
//! # Examples
//! ```
//! use actix_web::{get, web, test, App, HttpResponse, Error, Responder};
//!
//! #[get("/")]
//! async fn my_handler() -> Result<impl Responder, Error> {
//! Ok(HttpResponse::Ok())
//! }
//!
//! #[actix_rt::test]
//! async fn test_example() {
//! let srv = actix_test::start(||
//! App::new().service(my_handler)
//! );
//!
//! let req = srv.get("/");
//! let res = req.send().await.unwrap();
//!
//! assert!(res.status().is_success());
//! }
//! ```
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;
#[cfg(feature = "rustls")]
extern crate tls_rustls as rustls;
use std::{fmt, net, sync::mpsc, thread, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
pub use actix_http::test::TestBuffer;
use actix_http::{
http::{HeaderMap, Method},
ws, HttpService, Request, Response,
};
use actix_service::{map_config, IntoServiceFactory, ServiceFactory};
use actix_web::{
dev::{AppConfig, MessageBody, Server, Service},
rt, web, Error,
};
use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
use futures_core::Stream;
pub use actix_http_test::unused_addr;
pub use actix_web::test::{
call_service, default_service, init_service, load_stream, ok_service, read_body,
read_body_json, read_response, read_response_json, TestRequest,
};
/// Start default [`TestServer`].
///
/// # Examples
/// ```
/// use actix_web::{get, web, test, App, HttpResponse, Error, Responder};
///
/// #[get("/")]
/// async fn my_handler() -> Result<impl Responder, Error> {
/// Ok(HttpResponse::Ok())
/// }
///
/// #[actix_rt::test]
/// async fn test_example() {
/// let srv = actix_test::start(||
/// App::new().service(my_handler)
/// );
///
/// let req = srv.get("/");
/// let res = req.send().await.unwrap();
///
/// assert!(res.status().is_success());
/// }
/// ```
pub fn start<F, I, S, B>(factory: F) -> TestServer
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
start_with(TestServerConfig::default(), factory)
}
/// Start test server with custom configuration
///
/// Check [`TestServerConfig`] docs for configuration options.
///
/// # Examples
/// ```
/// use actix_web::{get, web, test, App, HttpResponse, Error, Responder};
///
/// #[get("/")]
/// async fn my_handler() -> Result<impl Responder, Error> {
/// Ok(HttpResponse::Ok())
/// }
///
/// #[actix_rt::test]
/// async fn test_example() {
/// let srv = actix_test::start_with(actix_test::config().h1(), ||
/// App::new().service(my_handler)
/// );
///
/// let req = srv.get("/");
/// let res = req.send().await.unwrap();
///
/// assert!(res.status().is_success());
/// }
/// ```
pub fn start_with<F, I, S, B>(cfg: TestServerConfig, factory: F) -> TestServer
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
let (tx, rx) = mpsc::channel();
let tls = match cfg.stream {
StreamType::Tcp => false,
#[cfg(feature = "openssl")]
StreamType::Openssl(_) => true,
#[cfg(feature = "rustls")]
StreamType::Rustls(_) => true,
};
// run server in separate thread
thread::spawn(move || {
let sys = rt::System::new();
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
let local_addr = tcp.local_addr().unwrap();
let factory = factory.clone();
let srv_cfg = cfg.clone();
let timeout = cfg.client_timeout;
let builder = Server::build().workers(1).disable_signals();
let srv = match srv_cfg.stream {
StreamType::Tcp => match srv_cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
HttpService::build()
.client_timeout(timeout)
.h1(map_config(factory(), move |_| app_cfg.clone()))
.tcp()
}),
HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
HttpService::build()
.client_timeout(timeout)
.h2(map_config(factory(), move |_| app_cfg.clone()))
.tcp()
}),
HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
HttpService::build()
.client_timeout(timeout)
.finish(map_config(factory(), move |_| app_cfg.clone()))
.tcp()
}),
},
#[cfg(feature = "openssl")]
StreamType::Openssl(acceptor) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
HttpService::build()
.client_timeout(timeout)
.h1(map_config(factory(), move |_| app_cfg.clone()))
.openssl(acceptor.clone())
}),
HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
HttpService::build()
.client_timeout(timeout)
.h2(map_config(factory(), move |_| app_cfg.clone()))
.openssl(acceptor.clone())
}),
HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
HttpService::build()
.client_timeout(timeout)
.finish(map_config(factory(), move |_| app_cfg.clone()))
.openssl(acceptor.clone())
}),
},
#[cfg(feature = "rustls")]
StreamType::Rustls(config) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
HttpService::build()
.client_timeout(timeout)
.h1(map_config(factory(), move |_| app_cfg.clone()))
.rustls(config.clone())
}),
HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
HttpService::build()
.client_timeout(timeout)
.h2(map_config(factory(), move |_| app_cfg.clone()))
.rustls(config.clone())
}),
HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
HttpService::build()
.client_timeout(timeout)
.finish(map_config(factory(), move |_| app_cfg.clone()))
.rustls(config.clone())
}),
},
}
.unwrap();
sys.block_on(async {
let srv = srv.run();
tx.send((rt::System::current(), srv, local_addr)).unwrap();
});
sys.run()
});
let (system, server, addr) = rx.recv().unwrap();
let client = {
let connector = {
#[cfg(feature = "openssl")]
{
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
builder.set_verify(SslVerifyMode::NONE);
let _ = builder
.set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
Connector::new()
.conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000))
.ssl(builder.build())
}
#[cfg(not(feature = "openssl"))]
{
Connector::new()
.conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000))
}
};
Client::builder().connector(connector).finish()
};
TestServer {
addr,
client,
system,
tls,
server,
}
}
#[derive(Debug, Clone)]
enum HttpVer {
Http1,
Http2,
Both,
}
#[derive(Clone)]
enum StreamType {
Tcp,
#[cfg(feature = "openssl")]
Openssl(openssl::ssl::SslAcceptor),
#[cfg(feature = "rustls")]
Rustls(rustls::ServerConfig),
}
/// Create default test server config.
pub fn config() -> TestServerConfig {
TestServerConfig::default()
}
#[derive(Clone)]
pub struct TestServerConfig {
tp: HttpVer,
stream: StreamType,
client_timeout: u64,
}
impl Default for TestServerConfig {
fn default() -> Self {
TestServerConfig::new()
}
}
impl TestServerConfig {
/// Create default server configuration
pub(crate) fn new() -> TestServerConfig {
TestServerConfig {
tp: HttpVer::Both,
stream: StreamType::Tcp,
client_timeout: 5000,
}
}
/// Accept HTTP/1.1 only.
pub fn h1(mut self) -> Self {
self.tp = HttpVer::Http1;
self
}
/// Accept HTTP/2 only.
pub fn h2(mut self) -> Self {
self.tp = HttpVer::Http2;
self
}
/// Accept secure connections via OpenSSL.
#[cfg(feature = "openssl")]
pub fn openssl(mut self, acceptor: openssl::ssl::SslAcceptor) -> Self {
self.stream = StreamType::Openssl(acceptor);
self
}
/// Accept secure connections via Rustls.
#[cfg(feature = "rustls")]
pub fn rustls(mut self, config: rustls::ServerConfig) -> Self {
self.stream = StreamType::Rustls(config);
self
}
/// Set client timeout in milliseconds for first request.
pub fn client_timeout(mut self, val: u64) -> Self {
self.client_timeout = val;
self
}
}
/// A basic HTTP server controller that simplifies the process of writing integration tests for
/// Actix Web applications.
///
/// See [`start`] for usage example.
pub struct TestServer {
addr: net::SocketAddr,
client: awc::Client,
system: rt::System,
tls: bool,
server: Server,
}
impl TestServer {
/// Construct test server url
pub fn addr(&self) -> net::SocketAddr {
self.addr
}
/// Construct test server url
pub fn url(&self, uri: &str) -> String {
let scheme = if self.tls { "https" } else { "http" };
if uri.starts_with('/') {
format!("{}://localhost:{}{}", scheme, self.addr.port(), uri)
} else {
format!("{}://localhost:{}/{}", scheme, self.addr.port(), uri)
}
}
/// Create `GET` request.
pub fn get(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.get(self.url(path.as_ref()).as_str())
}
/// Create `POST` request.
pub fn post(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.post(self.url(path.as_ref()).as_str())
}
/// Create `HEAD` request.
pub fn head(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.head(self.url(path.as_ref()).as_str())
}
/// Create `PUT` request.
pub fn put(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.put(self.url(path.as_ref()).as_str())
}
/// Create `PATCH` request.
pub fn patch(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.patch(self.url(path.as_ref()).as_str())
}
/// Create `DELETE` request.
pub fn delete(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.delete(self.url(path.as_ref()).as_str())
}
/// Create `OPTIONS` request.
pub fn options(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.options(self.url(path.as_ref()).as_str())
}
/// Connect request with given method and path.
pub fn request(&self, method: Method, path: impl AsRef<str>) -> ClientRequest {
self.client.request(method, path.as_ref())
}
pub async fn load_body<S>(
&mut self,
mut response: ClientResponse<S>,
) -> Result<web::Bytes, PayloadError>
where
S: Stream<Item = Result<web::Bytes, PayloadError>> + Unpin + 'static,
{
response.body().limit(10_485_760).await
}
/// Connect to WebSocket server at a given path.
pub async fn ws_at(
&mut self,
path: &str,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
let url = self.url(path);
let connect = self.client.ws(url).connect();
connect.await.map(|(_, framed)| framed)
}
/// Connect to a WebSocket server.
pub async fn ws(
&mut self,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
self.ws_at("/").await
}
/// Get default HeaderMap of Client.
///
/// Returns Some(&mut HeaderMap) when Client object is unique
/// (No other clone of client exists at the same time).
pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
self.client.headers()
}
/// Gracefully stop HTTP server.
pub async fn stop(self) {
self.server.stop(true).await;
self.system.stop();
rt::time::sleep(time::Duration::from_millis(100)).await;
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.system.stop()
}
}

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.4 - 2021-04-02
* No notable changes.
## 4.0.0-beta.3 - 2021-03-09 ## 4.0.0-beta.3 - 2021-03-09
* No notable changes. * No notable changes.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-actors" name = "actix-web-actors"
version = "4.0.0-beta.3" version = "4.0.0-beta.4"
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"
readme = "README.md" readme = "README.md"
@ -18,8 +18,8 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.11.0-beta.3", default-features = false } actix = { version = "0.11.0-beta.3", default-features = false }
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0-beta.1"
actix-http = "3.0.0-beta.4" actix-http = "3.0.0-beta.6"
actix-web = { version = "4.0.0-beta.4", default-features = false } actix-web = { version = "4.0.0-beta.6", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
@ -28,6 +28,9 @@ pin-project = "1.0.0"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.1" actix-rt = "2.2"
actix-test = "0.1.0-beta.1"
awc = { version = "3.0.0-beta.5", default-features = false }
env_logger = "0.8" env_logger = "0.8"
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.3)](https://docs.rs/actix-web-actors/4.0.0-beta.3) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.4)](https://docs.rs/actix-web-actors/4.0.0-beta.4)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.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.3/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.3) [![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.4)
[![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)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -22,9 +22,9 @@ use actix_http::{
http::HeaderValue, http::HeaderValue,
ws::{hash_key, Codec}, ws::{hash_key, Codec},
}; };
use actix_web::dev::HttpResponseBuilder;
use actix_web::error::{Error, PayloadError}; use actix_web::error::{Error, PayloadError};
use actix_web::http::{header, Method, StatusCode}; use actix_web::http::{header, Method, StatusCode};
use actix_web::HttpResponseBuilder;
use actix_web::{HttpRequest, HttpResponse}; use actix_web::{HttpRequest, HttpResponse};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use bytestring::ByteString; use bytestring::ByteString;

View File

@ -1,8 +1,11 @@
use actix::prelude::*; use actix::prelude::*;
use actix_web::{test, web, App, HttpRequest}; use actix_web::{
http::{header, StatusCode},
web, App, HttpRequest, HttpResponse,
};
use actix_web_actors::*; use actix_web_actors::*;
use bytes::Bytes; use bytes::Bytes;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt as _, StreamExt as _};
struct Ws; struct Ws;
@ -24,7 +27,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Ws {
#[actix_rt::test] #[actix_rt::test]
async fn test_simple() { async fn test_simple() {
let mut srv = test::start(|| { let mut srv = actix_test::start(|| {
App::new().service(web::resource("/").to( App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) }, |req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) },
)) ))
@ -56,3 +59,51 @@ async fn test_simple() {
let item = framed.next().await.unwrap().unwrap(); let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into()))); assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
} }
#[actix_rt::test]
async fn test_with_credentials() {
let mut srv = actix_test::start(|| {
App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move {
if req.headers().contains_key("Authorization") {
ws::start(Ws, &req, stream)
} else {
Ok(HttpResponse::new(StatusCode::UNAUTHORIZED))
}
},
))
});
// client service without credentials
match srv.ws().await {
Ok(_) => panic!("WebSocket client without credentials should panic"),
Err(awc::error::WsClientError::InvalidResponseStatus(status)) => {
assert_eq!(status, StatusCode::UNAUTHORIZED)
}
Err(e) => panic!("Invalid error from WebSocket client: {}", e),
}
let headers = srv.client_headers().unwrap();
headers.insert(
header::AUTHORIZATION,
header::HeaderValue::from_static("Bearer Something"),
);
// client service with credentials
let client = srv.ws();
let mut framed = client.await.unwrap();
framed.send(ws::Message::Text("text".into())).await.unwrap();
let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, ws::Frame::Text(Bytes::from_static(b"text")));
framed
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
.await
.unwrap();
let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
}

View File

@ -19,8 +19,11 @@ syn = { version = "1", features = ["full", "parsing"] }
proc-macro2 = "1" proc-macro2 = "1"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.1" actix-rt = "2.2"
actix-web = "4.0.0-beta.4" actix-test = "0.1.0-beta.1"
futures-util = { version = "0.3.7", default-features = false } actix-utils = "3.0.0"
actix-web = "4.0.0-beta.6"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
trybuild = "1" trybuild = "1"
rustversion = "1" rustversion = "1"

View File

@ -82,7 +82,7 @@ mod route;
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```
/// # 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")]
@ -127,7 +127,7 @@ code, e.g `my_guard` or `my_module::my_guard`.
# Example # Example
```rust ```
# use actix_web::HttpResponse; # use actix_web::HttpResponse;
# use actix_web_codegen::"#, stringify!($method), "; # use actix_web_codegen::"#, stringify!($method), ";
#[", stringify!($method), r#"("/")] #[", stringify!($method), r#"("/")]
@ -162,7 +162,7 @@ method_macro! {
/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. /// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications.
/// ///
/// # Examples /// # Examples
/// ```rust /// ```
/// #[actix_web_codegen::main] /// #[actix_web_codegen::main]
/// async fn main() { /// async fn main() {
/// async { println!("Hello world"); }.await /// async { println!("Hello world"); }.await

View File

@ -1,11 +1,17 @@
use std::future::Future; use std::future::Future;
use std::task::{Context, Poll};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_utils::future::{ok, Ready};
use actix_web::http::header::{HeaderName, HeaderValue}; use actix_web::{
use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder}; dev::{Service, ServiceRequest, ServiceResponse, Transform},
http::{
self,
header::{HeaderName, HeaderValue},
StatusCode,
},
web, App, Error, HttpResponse, Responder,
};
use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace}; use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace};
use futures_util::future::{self, LocalBoxFuture}; use futures_core::future::LocalBoxFuture;
// Make sure that we can name function as 'config' // Make sure that we can name function as 'config'
#[get("/config")] #[get("/config")]
@ -55,26 +61,26 @@ async fn trace_test() -> impl Responder {
#[get("/test")] #[get("/test")]
fn auto_async() -> impl Future<Output = Result<HttpResponse, actix_web::Error>> { fn auto_async() -> impl Future<Output = Result<HttpResponse, actix_web::Error>> {
future::ok(HttpResponse::Ok().finish()) ok(HttpResponse::Ok().finish())
} }
#[get("/test")] #[get("/test")]
fn auto_sync() -> impl Future<Output = Result<HttpResponse, actix_web::Error>> { fn auto_sync() -> impl Future<Output = Result<HttpResponse, actix_web::Error>> {
future::ok(HttpResponse::Ok().finish()) ok(HttpResponse::Ok().finish())
} }
#[put("/test/{param}")] #[put("/test/{param}")]
async fn put_param_test(_: Path<String>) -> impl Responder { async fn put_param_test(_: web::Path<String>) -> impl Responder {
HttpResponse::Created() HttpResponse::Created()
} }
#[delete("/test/{param}")] #[delete("/test/{param}")]
async fn delete_param_test(_: Path<String>) -> impl Responder { async fn delete_param_test(_: web::Path<String>) -> impl Responder {
HttpResponse::NoContent() HttpResponse::NoContent()
} }
#[get("/test/{param}")] #[get("/test/{param}")]
async fn get_param_test(_: Path<String>) -> impl Responder { async fn get_param_test(_: web::Path<String>) -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
} }
@ -102,10 +108,10 @@ where
type Error = Error; type Error = Error;
type Transform = ChangeStatusCodeMiddleware<S>; type Transform = ChangeStatusCodeMiddleware<S>;
type InitError = (); type InitError = ();
type Future = future::Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
future::ok(ChangeStatusCodeMiddleware { service }) ok(ChangeStatusCodeMiddleware { service })
} }
} }
@ -123,9 +129,7 @@ where
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { actix_web::dev::forward_ready!(service);
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let fut = self.service.call(req); let fut = self.service.call(req);
@ -142,13 +146,14 @@ where
} }
#[get("/test/wrap", wrap = "ChangeStatusCode")] #[get("/test/wrap", wrap = "ChangeStatusCode")]
async fn get_wrap(_: Path<String>) -> impl Responder { async fn get_wrap(_: web::Path<String>) -> impl Responder {
// panic!("actually never gets called because path failed to extract");
HttpResponse::Ok() HttpResponse::Ok()
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_params() { async fn test_params() {
let srv = test::start(|| { let srv = actix_test::start(|| {
App::new() App::new()
.service(get_param_test) .service(get_param_test)
.service(put_param_test) .service(put_param_test)
@ -170,7 +175,7 @@ async fn test_params() {
#[actix_rt::test] #[actix_rt::test]
async fn test_body() { async fn test_body() {
let srv = test::start(|| { let srv = actix_test::start(|| {
App::new() App::new()
.service(post_test) .service(post_test)
.service(put_test) .service(put_test)
@ -244,7 +249,7 @@ async fn test_body() {
#[actix_rt::test] #[actix_rt::test]
async fn test_auto_async() { async fn test_auto_async() {
let srv = test::start(|| App::new().service(auto_async)); let srv = actix_test::start(|| App::new().service(auto_async));
let request = srv.request(http::Method::GET, srv.url("/test")); let request = srv.request(http::Method::GET, srv.url("/test"));
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();
@ -253,9 +258,13 @@ async fn test_auto_async() {
#[actix_rt::test] #[actix_rt::test]
async fn test_wrap() { async fn test_wrap() {
let srv = test::start(|| App::new().service(get_wrap)); let srv = actix_test::start(|| App::new().service(get_wrap));
let request = srv.request(http::Method::GET, srv.url("/test/wrap")); let request = srv.request(http::Method::GET, srv.url("/test/wrap"));
let response = request.send().await.unwrap(); let mut response = request.send().await.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
assert!(response.headers().contains_key("custom-header")); assert!(response.headers().contains_key("custom-header"));
let body = response.body().await.unwrap();
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("wrong number of parameters"));
} }

View File

@ -1,7 +1,7 @@
use actix_web::{Responder, HttpResponse, App, test}; use actix_web::{Responder, HttpResponse, App};
use actix_web_codegen::*; use actix_web_codegen::*;
/// Docstrings shouldn't break anything. /// doc comments shouldn't break anything
#[get("/")] #[get("/")]
async fn index() -> impl Responder { async fn index() -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
@ -9,7 +9,7 @@ async fn index() -> impl Responder {
#[actix_web::main] #[actix_web::main]
async fn main() { async fn main() {
let srv = test::start(|| App::new().service(index)); let srv = actix_test::start(|| App::new().service(index));
let request = srv.get("/"); let request = srv.get("/");
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();

View File

@ -7,9 +7,9 @@ async fn index() -> String {
#[actix_web::main] #[actix_web::main]
async fn main() { async fn main() {
use actix_web::{App, test}; use actix_web::App;
let srv = test::start(|| App::new().service(index)); let srv = actix_test::start(|| App::new().service(index));
let request = srv.get("/"); let request = srv.get("/");
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();

View File

@ -5,7 +5,7 @@ error: HTTP method defined more than once: `GET`
| ^^^^^ | ^^^^^
error[E0425]: cannot find value `index` in this scope error[E0425]: cannot find value `index` in this scope
--> $DIR/route-duplicate-method-fail.rs:12:49 --> $DIR/route-duplicate-method-fail.rs:12:55
| |
12 | let srv = test::start(|| App::new().service(index)); 12 | let srv = actix_test::start(|| App::new().service(index));
| ^^^^^ not found in this scope | ^^^^^ not found in this scope

View File

@ -7,9 +7,9 @@ async fn index() -> String {
#[actix_web::main] #[actix_web::main]
async fn main() { async fn main() {
use actix_web::{App, test}; use actix_web::App;
let srv = test::start(|| App::new().service(index)); let srv = actix_test::start(|| App::new().service(index));
let request = srv.get("/"); let request = srv.get("/");
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();

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