mirror of https://github.com/fafhrd91/actix-web
Merge remote-tracking branch 'origin/master' into accept-encoding
This commit is contained in:
commit
63cb11301f
|
@ -0,0 +1,66 @@
|
||||||
|
name: CI (master only)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci_feature_powerset_check:
|
||||||
|
name: Verify Feature Combinations
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install stable
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: cargo-hack
|
||||||
|
|
||||||
|
- name: check feature combinations
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-all-feature-powerset }
|
||||||
|
|
||||||
|
- name: check feature combinations
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-all-feature-powerset-linux }
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
name: coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install stable
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Generate coverage file
|
||||||
|
run: |
|
||||||
|
cargo install cargo-tarpaulin --vers "^0.13"
|
||||||
|
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
||||||
|
- name: Upload to Codecov
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
with: { file: cobertura.xml }
|
|
@ -96,68 +96,6 @@ jobs:
|
||||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||||
cargo-cache
|
cargo-cache
|
||||||
|
|
||||||
ci_feature_powerset_check:
|
|
||||||
name: Verify Feature Combinations
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install stable
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable-x86_64-unknown-linux-gnu
|
|
||||||
profile: minimal
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with: { command: generate-lockfile }
|
|
||||||
- name: Cache Dependencies
|
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
|
||||||
|
|
||||||
- name: Install cargo-hack
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: install
|
|
||||||
args: cargo-hack
|
|
||||||
|
|
||||||
- name: check feature combinations
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with: { command: ci-check-all-feature-powerset }
|
|
||||||
|
|
||||||
- name: check feature combinations
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with: { command: ci-check-all-feature-powerset-linux }
|
|
||||||
|
|
||||||
coverage:
|
|
||||||
name: coverage
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install stable
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable-x86_64-unknown-linux-gnu
|
|
||||||
profile: minimal
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with: { command: generate-lockfile }
|
|
||||||
- name: Cache Dependencies
|
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
|
||||||
|
|
||||||
- name: Generate coverage file
|
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
run: |
|
|
||||||
cargo install cargo-tarpaulin --vers "^0.13"
|
|
||||||
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
|
||||||
- name: Upload to Codecov
|
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
uses: codecov/codecov-action@v1
|
|
||||||
with: { file: cobertura.xml }
|
|
||||||
|
|
||||||
rustdoc:
|
rustdoc:
|
||||||
name: doc tests
|
name: doc tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
26
CHANGES.md
26
CHANGES.md
|
@ -1,6 +1,32 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
### Added
|
||||||
|
- `guard::GuardContext` for use with the `Guard` trait. [#2552]
|
||||||
|
- `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `Guard` trait now receives a `&GuardContext`. [#2552]
|
||||||
|
- `guard::fn_guard` functions now receives a `&GuardContext`. [#2552]
|
||||||
|
- Some guards now return `impl Guard` and their concrete types are made private: `guard::{Header}` and all the method guards. [#2552]
|
||||||
|
- The `Not` guard is now generic over the type of guard it wraps. [#2552]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Rename `ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554]
|
||||||
|
- `ConnectionInfo::peer_addr` will not return the port number. [#2554]
|
||||||
|
- `ConnectionInfo::realip_remote_addr` will not return the port number if sourcing the IP from the peer's socket address. [#2554]
|
||||||
|
|
||||||
|
[#2552]: https://github.com/actix/actix-web/pull/2552
|
||||||
|
[#2554]: https://github.com/actix/actix-web/pull/2554
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.16 - 2021-12-27
|
||||||
|
### Changed
|
||||||
|
- No longer require `Scope` service body type to be boxed. [#2523]
|
||||||
|
- No longer require `Resource` service body type to be boxed. [#2526]
|
||||||
|
|
||||||
|
[#2523]: https://github.com/actix/actix-web/pull/2523
|
||||||
|
[#2526]: https://github.com/actix/actix-web/pull/2526
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.15 - 2021-12-17
|
## 4.0.0-beta.15 - 2021-12-17
|
||||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-web"
|
name = "actix-web"
|
||||||
version = "4.0.0-beta.15"
|
version = "4.0.0-beta.16"
|
||||||
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"
|
||||||
keywords = ["actix", "http", "web", "framework", "async"]
|
keywords = ["actix", "http", "web", "framework", "async"]
|
||||||
|
@ -72,12 +72,12 @@ experimental-io-uring = ["actix-server/io-uring"]
|
||||||
actix-codec = "0.4.1"
|
actix-codec = "0.4.1"
|
||||||
actix-macros = "0.2.3"
|
actix-macros = "0.2.3"
|
||||||
actix-rt = "2.3"
|
actix-rt = "2.3"
|
||||||
actix-server = "2.0.0-rc.1"
|
actix-server = "2.0.0-rc.2"
|
||||||
actix-service = "2.0.0"
|
actix-service = "2.0.0"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3.0.0"
|
||||||
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
|
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||||
|
|
||||||
actix-http = "3.0.0-beta.16"
|
actix-http = "3.0.0-beta.17"
|
||||||
actix-router = "0.5.0-beta.3"
|
actix-router = "0.5.0-beta.3"
|
||||||
actix-web-codegen = "0.5.0-beta.6"
|
actix-web-codegen = "0.5.0-beta.6"
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ derive_more = "0.99.5"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
itoa = "0.4"
|
itoa = "1"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
once_cell = "1.5"
|
once_cell = "1.5"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
@ -106,8 +106,8 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-test = { version = "0.1.0-beta.9", features = ["openssl", "rustls"] }
|
actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] }
|
||||||
awc = { version = "3.0.0-beta.14", features = ["openssl"] }
|
awc = { version = "3.0.0-beta.15", features = ["openssl"] }
|
||||||
|
|
||||||
brotli2 = "0.3.2"
|
brotli2 = "0.3.2"
|
||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web)
|
[](https://crates.io/crates/actix-web)
|
||||||
[](https://docs.rs/actix-web/4.0.0-beta.15)
|
[](https://docs.rs/actix-web/4.0.0-beta.16)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.15)
|
[](https://deps.rs/crate/actix-web/4.0.0-beta.16)
|
||||||
<br />
|
<br />
|
||||||
[](https://github.com/actix/actix-web/actions)
|
[](https://github.com/actix/actix-web/actions)
|
||||||
[](https://codecov.io/gh/actix/actix-web)
|
[](https://codecov.io/gh/actix/actix-web)
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.11 - 2021-12-27
|
||||||
|
* No significant changes since `0.6.0-beta.10`.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.10 - 2021-12-11
|
## 0.6.0-beta.10 - 2021-12-11
|
||||||
- No significant changes since `0.6.0-beta.9`.
|
- No significant changes since `0.6.0-beta.9`.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-files"
|
name = "actix-files"
|
||||||
version = "0.6.0-beta.10"
|
version = "0.6.0-beta.11"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"fakeshadow <24548779@qq.com>",
|
"fakeshadow <24548779@qq.com>",
|
||||||
|
@ -22,10 +22,10 @@ path = "src/lib.rs"
|
||||||
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-http = "3.0.0-beta.16"
|
actix-http = "3.0.0-beta.17"
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4.0.0-beta.15", default-features = false }
|
actix-web = { version = "4.0.0-beta.16", default-features = false }
|
||||||
|
|
||||||
askama_escape = "0.10"
|
askama_escape = "0.10"
|
||||||
bitflags = "1"
|
bitflags = "1"
|
||||||
|
@ -43,5 +43,5 @@ tokio-uring = { version = "0.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-test = "0.1.0-beta.9"
|
actix-test = "0.1.0-beta.10"
|
||||||
actix-web = "4.0.0-beta.15"
|
actix-web = "4.0.0-beta.16"
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
> Static file serving for Actix Web
|
> Static file serving for Actix Web
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://docs.rs/actix-files/0.6.0-beta.10)
|
[](https://docs.rs/actix-files/0.6.0-beta.11)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.10)
|
[](https://deps.rs/crate/actix-files/0.6.0-beta.11)
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
|
use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
|
||||||
|
|
||||||
use actix_service::Service;
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{ServiceRequest, ServiceResponse},
|
body::BoxBody,
|
||||||
|
dev::{Service, ServiceRequest, ServiceResponse},
|
||||||
error::Error,
|
error::Error,
|
||||||
guard::Guard,
|
guard::Guard,
|
||||||
http::{header, Method},
|
http::{header, Method},
|
||||||
|
@ -94,7 +94,7 @@ impl fmt::Debug for FilesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Service<ServiceRequest> for FilesService {
|
impl Service<ServiceRequest> for FilesService {
|
||||||
type Response = ServiceResponse;
|
type Response = ServiceResponse<BoxBody>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
let is_method_valid = if let Some(guard) = &self.guards {
|
let is_method_valid = if let Some(guard) = &self.guards {
|
||||||
// execute user defined guards
|
// execute user defined guards
|
||||||
(**guard).check(req.head())
|
(**guard).check(&req.guard_ctx())
|
||||||
} else {
|
} else {
|
||||||
// default behavior
|
// default behavior
|
||||||
matches!(*req.method(), Method::HEAD | Method::GET)
|
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||||
|
|
|
@ -3,6 +3,12 @@
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.10 - 2021-12-27
|
||||||
|
- Update `actix-server` to `2.0.0-rc.2`. [#2550]
|
||||||
|
|
||||||
|
[#2550]: https://github.com/actix/actix-web/pull/2550
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.9 - 2021-12-11
|
## 3.0.0-beta.9 - 2021-12-11
|
||||||
- No significant changes since `3.0.0-beta.8`.
|
- No significant changes since `3.0.0-beta.8`.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-http-test"
|
name = "actix-http-test"
|
||||||
version = "3.0.0-beta.9"
|
version = "3.0.0-beta.10"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Various helpers for Actix applications to use during testing"
|
description = "Various helpers for Actix applications to use during testing"
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
|
@ -31,11 +31,11 @@ openssl = ["tls-openssl", "awc/openssl"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2.0.0"
|
actix-service = "2.0.0"
|
||||||
actix-codec = "0.4.1"
|
actix-codec = "0.4.1"
|
||||||
actix-tls = "3.0.0-rc.1"
|
actix-tls = "3.0.0"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3.0.0"
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-server = "2.0.0-rc.1"
|
actix-server = "2.0.0-rc.2"
|
||||||
awc = { version = "3.0.0-beta.14", default-features = false }
|
awc = { version = "3.0.0-beta.15", default-features = false }
|
||||||
|
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
@ -48,8 +48,8 @@ serde_json = "1.0"
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||||
tokio = { version = "1.2", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] }
|
||||||
actix-http = "3.0.0-beta.16"
|
actix-http = "3.0.0-beta.17"
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
> Various helpers for Actix applications to use during testing.
|
> Various helpers for Actix applications to use during testing.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://docs.rs/actix-http-test/3.0.0-beta.9)
|
[](https://docs.rs/actix-http-test/3.0.0-beta.10)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||

|

|
||||||
<br>
|
<br>
|
||||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.9)
|
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.10)
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ use std::{net, thread, time::Duration};
|
||||||
|
|
||||||
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, ServerServiceFactory};
|
||||||
use awc::{
|
use awc::{
|
||||||
error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse,
|
error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse,
|
||||||
Connector,
|
Connector,
|
||||||
|
@ -51,13 +51,13 @@ use tokio::sync::mpsc;
|
||||||
/// assert!(response.status().is_success());
|
/// assert!(response.status().is_success());
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
pub async fn test_server<F: ServerServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
||||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
test_server_with_addr(tcp, factory).await
|
test_server_with_addr(tcp, factory).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start [`test server`](test_server()) on an existing address binding.
|
/// Start [`test server`](test_server()) on an existing address binding.
|
||||||
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
|
||||||
tcp: net::TcpListener,
|
tcp: net::TcpListener,
|
||||||
factory: F,
|
factory: F,
|
||||||
) -> TestServer {
|
) -> TestServer {
|
||||||
|
|
|
@ -2,11 +2,25 @@
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
### Changes
|
### Changes
|
||||||
- `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527]
|
|
||||||
- `impl Eq` for `header::ContentEncoding`. [#2501]
|
- `impl Eq` for `header::ContentEncoding`. [#2501]
|
||||||
|
|
||||||
[#2501]: https://github.com/actix/actix-web/pull/2501
|
[#2501]: https://github.com/actix/actix-web/pull/2501
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.17 - 2021-12-27
|
||||||
|
### Changes
|
||||||
|
- `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527]
|
||||||
|
- `Payload` inner fields are now named. [#2545]
|
||||||
|
- `impl Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545]
|
||||||
|
- `impl Future` for `h1::SendResponse` no longer requires the body type be `Unpin`. [#2545]
|
||||||
|
- `impl Stream` for `encoding::Decoder` no longer requires the stream type be `Unpin`. [#2545]
|
||||||
|
- Rename `PayloadStream` to `BoxedPayloadStream`. [#2545]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `h1::Payload::readany`. [#2545]
|
||||||
|
|
||||||
[#2527]: https://github.com/actix/actix-web/pull/2527
|
[#2527]: https://github.com/actix/actix-web/pull/2527
|
||||||
|
[#2545]: https://github.com/actix/actix-web/pull/2545
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.16 - 2021-12-17
|
## 3.0.0-beta.16 - 2021-12-17
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.0.0-beta.16"
|
version = "3.0.0-beta.17"
|
||||||
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"
|
||||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||||
|
@ -55,12 +55,11 @@ bytestring = "1"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
futures-task = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
|
||||||
h2 = "0.3.9"
|
h2 = "0.3.9"
|
||||||
http = "0.2.5"
|
http = "0.2.5"
|
||||||
httparse = "1.5.1"
|
httparse = "1.5.1"
|
||||||
httpdate = "1.0.1"
|
httpdate = "1.0.1"
|
||||||
itoa = "0.4"
|
itoa = "1"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
local-channel = "0.1"
|
local-channel = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
@ -68,11 +67,11 @@ mime = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
sha-1 = "0.9"
|
sha-1 = "0.10"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
|
|
||||||
# tls
|
# tls
|
||||||
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
|
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||||
|
|
||||||
# compression
|
# compression
|
||||||
brotli2 = { version="0.3.2", optional = true }
|
brotli2 = { version="0.3.2", optional = true }
|
||||||
|
@ -80,10 +79,10 @@ flate2 = { version = "1.0.13", optional = true }
|
||||||
zstd = { version = "0.9", optional = true }
|
zstd = { version = "0.9", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] }
|
actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] }
|
||||||
actix-server = "2.0.0-rc.1"
|
actix-server = "2.0.0-rc.2"
|
||||||
actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] }
|
actix-tls = { version = "3.0.0", features = ["openssl"] }
|
||||||
actix-web = "4.0.0-beta.15"
|
actix-web = "4.0.0-beta.16"
|
||||||
|
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
|
@ -97,7 +96,7 @@ serde_json = "1.0"
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||||
tokio = { version = "1.2", features = ["net", "rt", "macros"] }
|
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "ws"
|
name = "ws"
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
> HTTP primitives for the Actix ecosystem.
|
> HTTP primitives for the Actix ecosystem.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://docs.rs/actix-http/3.0.0-beta.16)
|
[](https://docs.rs/actix-http/3.0.0-beta.17)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.16)
|
[](https://deps.rs/crate/actix-http/3.0.0-beta.17)
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
|
|
@ -28,11 +28,14 @@ use crate::{
|
||||||
|
|
||||||
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
|
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
|
||||||
|
|
||||||
pub struct Decoder<S> {
|
pin_project_lite::pin_project! {
|
||||||
decoder: Option<ContentDecoder>,
|
pub struct Decoder<S> {
|
||||||
stream: S,
|
decoder: Option<ContentDecoder>,
|
||||||
eof: bool,
|
#[pin]
|
||||||
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
|
stream: S,
|
||||||
|
eof: bool,
|
||||||
|
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> Decoder<S>
|
impl<S> Decoder<S>
|
||||||
|
@ -89,42 +92,44 @@ where
|
||||||
|
|
||||||
impl<S> Stream for Decoder<S>
|
impl<S> Stream for Decoder<S>
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
{
|
{
|
||||||
type Item = Result<Bytes, PayloadError>;
|
type Item = Result<Bytes, PayloadError>;
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
let mut this = self.project();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(ref mut fut) = self.fut {
|
if let Some(ref mut fut) = this.fut {
|
||||||
let (chunk, decoder) =
|
let (chunk, decoder) =
|
||||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||||
|
|
||||||
self.decoder = Some(decoder);
|
*this.decoder = Some(decoder);
|
||||||
self.fut.take();
|
this.fut.take();
|
||||||
|
|
||||||
if let Some(chunk) = chunk {
|
if let Some(chunk) = chunk {
|
||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.eof {
|
if *this.eof {
|
||||||
return Poll::Ready(None);
|
return Poll::Ready(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
match ready!(Pin::new(&mut self.stream).poll_next(cx)) {
|
match ready!(this.stream.as_mut().poll_next(cx)) {
|
||||||
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||||
|
|
||||||
Some(Ok(chunk)) => {
|
Some(Ok(chunk)) => {
|
||||||
if let Some(mut decoder) = self.decoder.take() {
|
if let Some(mut decoder) = this.decoder.take() {
|
||||||
if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
|
if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
|
||||||
let chunk = decoder.feed_data(chunk)?;
|
let chunk = decoder.feed_data(chunk)?;
|
||||||
self.decoder = Some(decoder);
|
*this.decoder = Some(decoder);
|
||||||
|
|
||||||
if let Some(chunk) = chunk {
|
if let Some(chunk) = chunk {
|
||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.fut = Some(spawn_blocking(move || {
|
*this.fut = Some(spawn_blocking(move || {
|
||||||
let chunk = decoder.feed_data(chunk)?;
|
let chunk = decoder.feed_data(chunk)?;
|
||||||
Ok((chunk, decoder))
|
Ok((chunk, decoder))
|
||||||
}));
|
}));
|
||||||
|
@ -137,9 +142,9 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
self.eof = true;
|
*this.eof = true;
|
||||||
|
|
||||||
return if let Some(mut decoder) = self.decoder.take() {
|
return if let Some(mut decoder) = this.decoder.take() {
|
||||||
match decoder.feed_eof() {
|
match decoder.feed_eof() {
|
||||||
Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
|
Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
|
||||||
Ok(None) => Poll::Ready(None),
|
Ok(None) => Poll::Ready(None),
|
||||||
|
|
|
@ -646,10 +646,11 @@ where
|
||||||
Payload is attached to Request and passed to Service::call
|
Payload is attached to Request and passed to Service::call
|
||||||
where the state can be collected and consumed.
|
where the state can be collected and consumed.
|
||||||
*/
|
*/
|
||||||
let (ps, pl) = Payload::create(false);
|
let (sender, payload) = Payload::create(false);
|
||||||
let (req1, _) = req.replace_payload(crate::Payload::H1(pl));
|
let (req1, _) =
|
||||||
|
req.replace_payload(crate::Payload::H1 { payload });
|
||||||
req = req1;
|
req = req1;
|
||||||
*this.payload = Some(ps);
|
*this.payload = Some(sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request has no payload.
|
// Request has no payload.
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
//! Payload stream
|
//! Payload stream
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::VecDeque;
|
use std::{
|
||||||
use std::pin::Pin;
|
cell::RefCell,
|
||||||
use std::rc::{Rc, Weak};
|
collections::VecDeque,
|
||||||
use std::task::{Context, Poll, Waker};
|
pin::Pin,
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
task::{Context, Poll, Waker},
|
||||||
|
};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
|
@ -22,39 +25,32 @@ pub enum PayloadStatus {
|
||||||
|
|
||||||
/// Buffered stream of bytes chunks
|
/// Buffered stream of bytes chunks
|
||||||
///
|
///
|
||||||
/// Payload stores chunks in a vector. First chunk can be received with
|
/// Payload stores chunks in a vector. First chunk can be received with `poll_next`. Payload does
|
||||||
/// `.readany()` method. Payload stream is not thread safe. Payload does not
|
/// not notify current task when new data is available.
|
||||||
/// notify current task when new data is available.
|
|
||||||
///
|
///
|
||||||
/// Payload stream can be used as `Response` body stream.
|
/// Payload can be used as `Response` body stream.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Payload {
|
pub struct Payload {
|
||||||
inner: Rc<RefCell<Inner>>,
|
inner: Rc<RefCell<Inner>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Payload {
|
impl Payload {
|
||||||
/// Create payload stream.
|
/// Creates a payload stream.
|
||||||
///
|
///
|
||||||
/// This method construct two objects responsible for bytes stream
|
/// This method construct two objects responsible for bytes stream generation:
|
||||||
/// generation.
|
/// - `PayloadSender` - *Sender* side of the stream
|
||||||
///
|
/// - `Payload` - *Receiver* side of the stream
|
||||||
/// * `PayloadSender` - *Sender* side of the stream
|
|
||||||
///
|
|
||||||
/// * `Payload` - *Receiver* side of the stream
|
|
||||||
pub fn create(eof: bool) -> (PayloadSender, Payload) {
|
pub fn create(eof: bool) -> (PayloadSender, Payload) {
|
||||||
let shared = Rc::new(RefCell::new(Inner::new(eof)));
|
let shared = Rc::new(RefCell::new(Inner::new(eof)));
|
||||||
|
|
||||||
(
|
(
|
||||||
PayloadSender {
|
PayloadSender::new(Rc::downgrade(&shared)),
|
||||||
inner: Rc::downgrade(&shared),
|
|
||||||
},
|
|
||||||
Payload { inner: shared },
|
Payload { inner: shared },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create empty payload
|
/// Creates an empty payload.
|
||||||
#[doc(hidden)]
|
pub(crate) fn empty() -> Payload {
|
||||||
pub fn empty() -> Payload {
|
|
||||||
Payload {
|
Payload {
|
||||||
inner: Rc::new(RefCell::new(Inner::new(true))),
|
inner: Rc::new(RefCell::new(Inner::new(true))),
|
||||||
}
|
}
|
||||||
|
@ -77,14 +73,6 @@ impl Payload {
|
||||||
pub fn unread_data(&mut self, data: Bytes) {
|
pub fn unread_data(&mut self, data: Bytes) {
|
||||||
self.inner.borrow_mut().unread_data(data);
|
self.inner.borrow_mut().unread_data(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn readany(
|
|
||||||
&mut self,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
|
||||||
self.inner.borrow_mut().readany(cx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for Payload {
|
impl Stream for Payload {
|
||||||
|
@ -94,7 +82,7 @@ impl Stream for Payload {
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
||||||
self.inner.borrow_mut().readany(cx)
|
Pin::new(&mut *self.inner.borrow_mut()).poll_next(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +92,10 @@ pub struct PayloadSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PayloadSender {
|
impl PayloadSender {
|
||||||
|
fn new(inner: Weak<RefCell<Inner>>) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_error(&mut self, err: PayloadError) {
|
pub fn set_error(&mut self, err: PayloadError) {
|
||||||
if let Some(shared) = self.inner.upgrade() {
|
if let Some(shared) = self.inner.upgrade() {
|
||||||
|
@ -227,7 +219,10 @@ impl Inner {
|
||||||
self.len
|
self.len
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readany(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
||||||
if let Some(data) = self.items.pop_front() {
|
if let Some(data) = self.items.pop_front() {
|
||||||
self.len -= data.len();
|
self.len -= data.len();
|
||||||
self.need_read = self.len < MAX_BUFFER_SIZE;
|
self.need_read = self.len < MAX_BUFFER_SIZE;
|
||||||
|
@ -257,8 +252,18 @@ impl Inner {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
|
||||||
use actix_utils::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
|
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
assert_impl_all!(Payload: Unpin);
|
||||||
|
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
|
assert_impl_all!(Inner: Unpin, Send, Sync);
|
||||||
|
assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_unread_data() {
|
async fn test_unread_data() {
|
||||||
|
@ -270,7 +275,10 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Bytes::from("data"),
|
Bytes::from("data"),
|
||||||
poll_fn(|cx| payload.readany(cx)).await.unwrap().unwrap()
|
poll_fn(|cx| Pin::new(&mut payload).poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ where
|
||||||
impl<T, B> Future for SendResponse<T, B>
|
impl<T, B> Future for SendResponse<T, B>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
B: MessageBody + Unpin,
|
B: MessageBody,
|
||||||
B::Error: Into<Error>,
|
B::Error: Into<Error>,
|
||||||
{
|
{
|
||||||
type Output = Result<Framed<T, Codec>, Error>;
|
type Output = Result<Framed<T, Codec>, Error>;
|
||||||
|
@ -81,7 +81,7 @@ where
|
||||||
// body is done when item is None
|
// body is done when item is None
|
||||||
body_done = item.is_none();
|
body_done = item.is_none();
|
||||||
if body_done {
|
if body_done {
|
||||||
let _ = this.body.take();
|
this.body.set(None);
|
||||||
}
|
}
|
||||||
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
||||||
framed
|
framed
|
||||||
|
|
|
@ -108,8 +108,8 @@ where
|
||||||
match Pin::new(&mut this.connection).poll_accept(cx)? {
|
match Pin::new(&mut this.connection).poll_accept(cx)? {
|
||||||
Poll::Ready(Some((req, tx))) => {
|
Poll::Ready(Some((req, tx))) => {
|
||||||
let (parts, body) = req.into_parts();
|
let (parts, body) = req.into_parts();
|
||||||
let pl = crate::h2::Payload::new(body);
|
let payload = crate::h2::Payload::new(body);
|
||||||
let pl = Payload::H2(pl);
|
let pl = Payload::H2 { payload };
|
||||||
let mut req = Request::with_payload(pl);
|
let mut req = Request::with_payload(pl);
|
||||||
|
|
||||||
let head = req.head_mut();
|
let head = req.head_mut();
|
||||||
|
@ -288,9 +288,11 @@ fn prepare_response(
|
||||||
let _ = match size {
|
let _ = match size {
|
||||||
BodySize::None | BodySize::Stream => None,
|
BodySize::None | BodySize::Stream => None,
|
||||||
|
|
||||||
BodySize::Sized(0) => res
|
BodySize::Sized(0) => {
|
||||||
.headers_mut()
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
|
const HV_ZERO: HeaderValue = HeaderValue::from_static("0");
|
||||||
|
res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
BodySize::Sized(len) => {
|
BodySize::Sized(len) => {
|
||||||
let mut buf = itoa::Buffer::new();
|
let mut buf = itoa::Buffer::new();
|
||||||
|
|
|
@ -98,3 +98,14 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
|
||||||
|
use static_assertions::assert_impl_all;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
assert_impl_all!(Payload: Unpin, Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
|
}
|
||||||
|
|
|
@ -45,13 +45,13 @@ pub enum ContentEncoding {
|
||||||
impl ContentEncoding {
|
impl ContentEncoding {
|
||||||
/// Is the content compressed?
|
/// Is the content compressed?
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_compression(self) -> bool {
|
pub const fn is_compression(self) -> bool {
|
||||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert content encoding to string.
|
/// Convert content encoding to string.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn as_str(self) -> &'static str {
|
pub const fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
ContentEncoding::Br => "br",
|
ContentEncoding::Br => "br",
|
||||||
ContentEncoding::Gzip => "gzip",
|
ContentEncoding::Gzip => "gzip",
|
||||||
|
|
|
@ -87,7 +87,7 @@ impl fmt::Display for Quality {
|
||||||
|
|
||||||
// 0 is already handled so it's not possible to have a trailing 0 in this range
|
// 0 is already handled so it's not possible to have a trailing 0 in this range
|
||||||
// we can just write the integer
|
// we can just write the integer
|
||||||
itoa::fmt(f, x)
|
itoa_fmt(f, x)
|
||||||
} else if x < 100 {
|
} else if x < 100 {
|
||||||
// x in is range 10–99
|
// x in is range 10–99
|
||||||
|
|
||||||
|
@ -95,21 +95,21 @@ impl fmt::Display for Quality {
|
||||||
|
|
||||||
if x % 10 == 0 {
|
if x % 10 == 0 {
|
||||||
// trailing 0, divide by 10 and write
|
// trailing 0, divide by 10 and write
|
||||||
itoa::fmt(f, x / 10)
|
itoa_fmt(f, x / 10)
|
||||||
} else {
|
} else {
|
||||||
itoa::fmt(f, x)
|
itoa_fmt(f, x)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// x is in range 100–999
|
// x is in range 100–999
|
||||||
|
|
||||||
if x % 100 == 0 {
|
if x % 100 == 0 {
|
||||||
// two trailing 0s, divide by 100 and write
|
// two trailing 0s, divide by 100 and write
|
||||||
itoa::fmt(f, x / 100)
|
itoa_fmt(f, x / 100)
|
||||||
} else if x % 10 == 0 {
|
} else if x % 10 == 0 {
|
||||||
// one trailing 0, divide by 10 and write
|
// one trailing 0, divide by 10 and write
|
||||||
itoa::fmt(f, x / 10)
|
itoa_fmt(f, x / 10)
|
||||||
} else {
|
} else {
|
||||||
itoa::fmt(f, x)
|
itoa_fmt(f, x)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,6 +117,12 @@ impl fmt::Display for Quality {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write integer to a `fmt::Write`.
|
||||||
|
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
|
||||||
|
let mut buf = itoa::Buffer::new();
|
||||||
|
wr.write_str(buf.format(value))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Display, Error)]
|
#[derive(Debug, Clone, Display, Error)]
|
||||||
#[display(fmt = "quality out of bounds")]
|
#[display(fmt = "quality out of bounds")]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
|
|
|
@ -58,7 +58,8 @@ pub use self::header::ContentEncoding;
|
||||||
pub use self::http_message::HttpMessage;
|
pub use self::http_message::HttpMessage;
|
||||||
pub use self::message::ConnectionType;
|
pub use self::message::ConnectionType;
|
||||||
pub use self::message::Message;
|
pub use self::message::Message;
|
||||||
pub use self::payload::{Payload, PayloadStream};
|
#[allow(deprecated)]
|
||||||
|
pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream};
|
||||||
pub use self::requests::{Request, RequestHead, RequestHeadType};
|
pub use self::requests::{Request, RequestHead, RequestHeadType};
|
||||||
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
||||||
pub use self::service::HttpService;
|
pub use self::service::HttpService;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, ops, rc::Rc};
|
||||||
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ impl<T: Head> Message<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Head> std::ops::Deref for Message<T> {
|
impl<T: Head> ops::Deref for Message<T> {
|
||||||
type Target = T;
|
type Target = T;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
@ -57,7 +57,7 @@ impl<T: Head> std::ops::Deref for Message<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Head> std::ops::DerefMut for Message<T> {
|
impl<T: Head> ops::DerefMut for Message<T> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
Rc::get_mut(&mut self.head).expect("Multiple copies exist")
|
Rc::get_mut(&mut self.head).expect("Multiple copies exist")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,89 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
mem,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
use h2::RecvStream;
|
|
||||||
|
|
||||||
use crate::error::PayloadError;
|
use crate::error::PayloadError;
|
||||||
|
|
||||||
// TODO: rename to boxed payload
|
/// A boxed payload stream.
|
||||||
/// A boxed payload.
|
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
||||||
pub type PayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
|
||||||
|
|
||||||
/// A streaming payload.
|
#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
||||||
pub enum Payload<S = PayloadStream> {
|
pub type PayloadStream = BoxedPayloadStream;
|
||||||
None,
|
|
||||||
H1(crate::h1::Payload),
|
pin_project_lite::pin_project! {
|
||||||
H2(crate::h2::Payload),
|
/// A streaming payload.
|
||||||
Stream(S),
|
#[project = PayloadProj]
|
||||||
|
pub enum Payload<S = BoxedPayloadStream> {
|
||||||
|
None,
|
||||||
|
H1 { payload: crate::h1::Payload },
|
||||||
|
H2 { payload: crate::h2::Payload },
|
||||||
|
Stream { #[pin] payload: S },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> From<crate::h1::Payload> for Payload<S> {
|
impl<S> From<crate::h1::Payload> for Payload<S> {
|
||||||
fn from(v: crate::h1::Payload) -> Self {
|
fn from(payload: crate::h1::Payload) -> Self {
|
||||||
Payload::H1(v)
|
Payload::H1 { payload }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> From<crate::h2::Payload> for Payload<S> {
|
impl<S> From<crate::h2::Payload> for Payload<S> {
|
||||||
fn from(v: crate::h2::Payload) -> Self {
|
fn from(payload: crate::h2::Payload) -> Self {
|
||||||
Payload::H2(v)
|
Payload::H2 { payload }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> From<RecvStream> for Payload<S> {
|
impl<S> From<h2::RecvStream> for Payload<S> {
|
||||||
fn from(v: RecvStream) -> Self {
|
fn from(stream: h2::RecvStream) -> Self {
|
||||||
Payload::H2(crate::h2::Payload::new(v))
|
Payload::H2 {
|
||||||
|
payload: crate::h2::Payload::new(stream),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PayloadStream> for Payload {
|
impl From<BoxedPayloadStream> for Payload {
|
||||||
fn from(pl: PayloadStream) -> Self {
|
fn from(payload: BoxedPayloadStream) -> Self {
|
||||||
Payload::Stream(pl)
|
Payload::Stream { payload }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> Payload<S> {
|
impl<S> Payload<S> {
|
||||||
/// Takes current payload and replaces it with `None` value
|
/// Takes current payload and replaces it with `None` value
|
||||||
pub fn take(&mut self) -> Payload<S> {
|
pub fn take(&mut self) -> Payload<S> {
|
||||||
std::mem::replace(self, Payload::None)
|
mem::replace(self, Payload::None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> Stream for Payload<S>
|
impl<S> Stream for Payload<S>
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
{
|
{
|
||||||
type Item = Result<Bytes, PayloadError>;
|
type Item = Result<Bytes, PayloadError>;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
match self.get_mut() {
|
match self.project() {
|
||||||
Payload::None => Poll::Ready(None),
|
PayloadProj::None => Poll::Ready(None),
|
||||||
Payload::H1(ref mut pl) => pl.readany(cx),
|
PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx),
|
||||||
Payload::H2(ref mut pl) => Pin::new(pl).poll_next(cx),
|
PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx),
|
||||||
Payload::Stream(ref mut pl) => Pin::new(pl).poll_next(cx),
|
PayloadProj::Stream { payload } => payload.poll_next(cx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
|
||||||
|
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
assert_impl_all!(Payload: Unpin);
|
||||||
|
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
|
}
|
||||||
|
|
|
@ -10,11 +10,12 @@ use std::{
|
||||||
use http::{header, Method, Uri, Version};
|
use http::{header, Method, Uri, Version};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
header::HeaderMap, Extensions, HttpMessage, Message, Payload, PayloadStream, RequestHead,
|
header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload,
|
||||||
|
RequestHead,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An HTTP request.
|
/// An HTTP request.
|
||||||
pub struct Request<P = PayloadStream> {
|
pub struct Request<P = BoxedPayloadStream> {
|
||||||
pub(crate) payload: Payload<P>,
|
pub(crate) payload: Payload<P>,
|
||||||
pub(crate) head: Message<RequestHead>,
|
pub(crate) head: Message<RequestHead>,
|
||||||
pub(crate) conn_data: Option<Rc<Extensions>>,
|
pub(crate) conn_data: Option<Rc<Extensions>>,
|
||||||
|
@ -46,7 +47,7 @@ impl<P> HttpMessage for Request<P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Message<RequestHead>> for Request<PayloadStream> {
|
impl From<Message<RequestHead>> for Request<BoxedPayloadStream> {
|
||||||
fn from(head: Message<RequestHead>) -> Self {
|
fn from(head: Message<RequestHead>) -> Self {
|
||||||
Request {
|
Request {
|
||||||
head,
|
head,
|
||||||
|
@ -57,10 +58,10 @@ impl From<Message<RequestHead>> for Request<PayloadStream> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Request<PayloadStream> {
|
impl Request<BoxedPayloadStream> {
|
||||||
/// Create new Request instance
|
/// Create new Request instance
|
||||||
#[allow(clippy::new_without_default)]
|
#[allow(clippy::new_without_default)]
|
||||||
pub fn new() -> Request<PayloadStream> {
|
pub fn new() -> Request<BoxedPayloadStream> {
|
||||||
Request {
|
Request {
|
||||||
head: Message::new(),
|
head: Message::new(),
|
||||||
payload: Payload::None,
|
payload: Payload::None,
|
||||||
|
|
|
@ -120,7 +120,7 @@ impl TestRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set request payload.
|
/// Set request payload.
|
||||||
pub fn set_payload<B: Into<Bytes>>(&mut self, data: B) -> &mut Self {
|
pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self {
|
||||||
let mut payload = crate::h1::Payload::empty();
|
let mut payload = crate::h1::Payload::empty();
|
||||||
payload.unread_data(data.into());
|
payload.unread_data(data.into());
|
||||||
parts(&mut self.0).payload = Some(payload.into());
|
parts(&mut self.0).payload = Some(payload.into());
|
||||||
|
|
|
@ -99,8 +99,9 @@ impl From<HandshakeError> for Response<BoxBody> {
|
||||||
match err {
|
match err {
|
||||||
HandshakeError::GetMethodRequired => {
|
HandshakeError::GetMethodRequired => {
|
||||||
let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED);
|
let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED);
|
||||||
res.headers_mut()
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
.insert(header::ALLOW, HeaderValue::from_static("GET"));
|
const HV_GET: HeaderValue = HeaderValue::from_static("GET");
|
||||||
|
res.headers_mut().insert(header::ALLOW, HV_GET);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::{
|
||||||
io::{self, BufReader, Write},
|
io::{self, BufReader, Write},
|
||||||
net::{SocketAddr, TcpStream as StdTcpStream},
|
net::{SocketAddr, TcpStream as StdTcpStream},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
task::Poll,
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
|
@ -16,25 +17,37 @@ use actix_http::{
|
||||||
Error, HttpService, Method, Request, Response, StatusCode, Version,
|
Error, HttpService, Method, Request, Response, StatusCode, Version,
|
||||||
};
|
};
|
||||||
use actix_http_test::test_server;
|
use actix_http_test::test_server;
|
||||||
|
use actix_rt::pin;
|
||||||
use actix_service::{fn_factory_with_config, fn_service};
|
use actix_service::{fn_factory_with_config, fn_service};
|
||||||
use actix_tls::connect::rustls::webpki_roots_cert_store;
|
use actix_tls::connect::rustls::webpki_roots_cert_store;
|
||||||
use actix_utils::future::{err, ok};
|
use actix_utils::future::{err, ok, poll_fn};
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use derive_more::{Display, Error};
|
use derive_more::{Display, Error};
|
||||||
use futures_core::Stream;
|
use futures_core::{ready, Stream};
|
||||||
use futures_util::stream::{once, StreamExt as _};
|
use futures_util::stream::once;
|
||||||
use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName};
|
use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName};
|
||||||
use rustls_pemfile::{certs, pkcs8_private_keys};
|
use rustls_pemfile::{certs, pkcs8_private_keys};
|
||||||
|
|
||||||
async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError>
|
async fn load_body<S>(stream: S) -> Result<BytesMut, PayloadError>
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
{
|
{
|
||||||
let mut body = BytesMut::new();
|
let mut buf = BytesMut::new();
|
||||||
while let Some(item) = stream.next().await {
|
|
||||||
body.extend_from_slice(&item?)
|
pin!(stream);
|
||||||
}
|
|
||||||
Ok(body)
|
poll_fn(|cx| loop {
|
||||||
|
let body = stream.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tls_config() -> RustlsServerConfig {
|
fn tls_config() -> RustlsServerConfig {
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 0.4.0-beta.11 - 2021-12-27
|
||||||
|
* No significant changes since `0.4.0-beta.10`.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.10 - 2021-12-11
|
## 0.4.0-beta.10 - 2021-12-11
|
||||||
- No significant changes since `0.4.0-beta.9`.
|
- No significant changes since `0.4.0-beta.9`.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-multipart"
|
name = "actix-multipart"
|
||||||
version = "0.4.0-beta.10"
|
version = "0.4.0-beta.11"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Multipart form support for Actix Web"
|
description = "Multipart form support for Actix Web"
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
|
@ -15,7 +15,7 @@ path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3.0.0"
|
||||||
actix-web = { version = "4.0.0-beta.15", default-features = false }
|
actix-web = { version = "4.0.0-beta.16", default-features = false }
|
||||||
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
|
@ -28,7 +28,7 @@ twoway = "0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-http = "3.0.0-beta.16"
|
actix-http = "3.0.0-beta.17"
|
||||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
tokio = { version = "1", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
> Multipart form support for Actix Web.
|
> Multipart form support for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-multipart)
|
[](https://crates.io/crates/actix-multipart)
|
||||||
[](https://docs.rs/actix-multipart/0.4.0-beta.10)
|
[](https://docs.rs/actix-multipart/0.4.0-beta.11)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.10)
|
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.11)
|
||||||
[](https://crates.io/crates/actix-multipart)
|
[](https://crates.io/crates/actix-multipart)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
|
|
@ -1233,7 +1233,7 @@ mod tests {
|
||||||
|
|
||||||
// and should not consume the payload
|
// and should not consume the payload
|
||||||
match payload {
|
match payload {
|
||||||
actix_web::dev::Payload::H1(_) => {} //expected
|
actix_web::dev::Payload::H1 { .. } => {} //expected
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ default = ["http"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytestring = ">=0.1.5, <2"
|
bytestring = ">=0.1.5, <2"
|
||||||
firestorm = "0.4"
|
firestorm = "0.5"
|
||||||
http = { version = "0.2.3", optional = true }
|
http = { version = "0.2.3", optional = true }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
|
@ -29,7 +29,7 @@ serde = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
firestorm = { version = "0.4", features = ["enable_system_time"] }
|
firestorm = { version = "0.5", features = ["enable_system_time"] }
|
||||||
http = "0.2.5"
|
http = "0.2.5"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
|
|
@ -29,26 +29,25 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// # Pattern Format and Matching Behavior
|
/// # Pattern Format and Matching Behavior
|
||||||
///
|
|
||||||
/// Resource pattern is defined as a string of zero or more _segments_ where each segment is
|
/// Resource pattern is defined as a string of zero or more _segments_ where each segment is
|
||||||
/// preceded by a slash `/`.
|
/// preceded by a slash `/`.
|
||||||
///
|
///
|
||||||
/// This means that pattern string __must__ either be empty or begin with a slash (`/`).
|
/// This means that pattern string __must__ either be empty or begin with a slash (`/`). This also
|
||||||
/// This also implies that a trailing slash in pattern defines an empty segment.
|
/// implies that a trailing slash in pattern defines an empty segment. For example, the pattern
|
||||||
/// For example, the pattern `"/user/"` has two segments: `["user", ""]`
|
/// `"/user/"` has two segments: `["user", ""]`
|
||||||
///
|
///
|
||||||
/// A key point to underhand is that `ResourceDef` matches segments, not strings.
|
/// A key point to understand is that `ResourceDef` matches segments, not strings. Segments are
|
||||||
/// It matches segments individually.
|
/// matched individually. For example, the pattern `/user/` is not considered a prefix for the path
|
||||||
/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`,
|
/// `/user/123/456`, because the second segment doesn't match: `["user", ""]`
|
||||||
/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`.
|
/// vs `["user", "123", "456"]`.
|
||||||
///
|
///
|
||||||
/// This definition is consistent with the definition of absolute URL path in
|
/// This definition is consistent with the definition of absolute URL path in
|
||||||
/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
|
/// [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// # Static Resources
|
/// # Static Resources
|
||||||
/// A static resource is the most basic type of definition. Pass a pattern to
|
/// A static resource is the most basic type of definition. Pass a pattern to [new][Self::new].
|
||||||
/// [new][Self::new]. Conforming paths must match the pattern exactly.
|
/// Conforming paths must match the pattern exactly.
|
||||||
///
|
///
|
||||||
/// ## Examples
|
/// ## Examples
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -63,7 +62,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||||
/// assert!(!resource.is_match("/search"));
|
/// assert!(!resource.is_match("/search"));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
|
||||||
/// # Dynamic Segments
|
/// # Dynamic Segments
|
||||||
/// Also known as "path parameters". Resources can define sections of a pattern that be extracted
|
/// Also known as "path parameters". Resources can define sections of a pattern that be extracted
|
||||||
/// from a conforming path, if it conforms to (one of) the resource pattern(s).
|
/// from a conforming path, if it conforms to (one of) the resource pattern(s).
|
||||||
|
@ -102,15 +100,15 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||||
/// assert_eq!(path.get("id").unwrap(), "123");
|
/// assert_eq!(path.get("id").unwrap(), "123");
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
|
||||||
/// # Prefix Resources
|
/// # Prefix Resources
|
||||||
/// A prefix resource is defined as pattern that can match just the start of a path, up to a
|
/// A prefix resource is defined as pattern that can match just the start of a path, up to a
|
||||||
/// segment boundary.
|
/// segment boundary.
|
||||||
///
|
///
|
||||||
/// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior.
|
/// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior.
|
||||||
/// They define and therefore require an empty segment in order to match. Examples are given below.
|
/// They define and therefore require an empty segment in order to match. It is easier to understand
|
||||||
|
/// this behavior after reading the [matching behavior section]. Examples are given below.
|
||||||
///
|
///
|
||||||
/// Empty pattern matches any path as a prefix.
|
/// The empty pattern (`""`), as a prefix, matches any path.
|
||||||
///
|
///
|
||||||
/// Prefix resources can contain dynamic segments.
|
/// Prefix resources can contain dynamic segments.
|
||||||
///
|
///
|
||||||
|
@ -130,7 +128,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||||
/// assert!(!resource.is_match("/user/123"));
|
/// assert!(!resource.is_match("/user/123"));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
|
||||||
/// # Custom Regex Segments
|
/// # Custom Regex Segments
|
||||||
/// Dynamic segments can be customised to only match a specific regular expression. It can be
|
/// Dynamic segments can be customised to only match a specific regular expression. It can be
|
||||||
/// helpful to do this if resource definitions would otherwise conflict and cause one to
|
/// helpful to do this if resource definitions would otherwise conflict and cause one to
|
||||||
|
@ -158,7 +155,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||||
/// assert!(!resource.is_match("/user/abc"));
|
/// assert!(!resource.is_match("/user/abc"));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
|
||||||
/// # Tail Segments
|
/// # Tail Segments
|
||||||
/// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those
|
/// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those
|
||||||
/// up until a `/` character), there is a special pattern to match (and capture) the remaining
|
/// up until a `/` character), there is a special pattern to match (and capture) the remaining
|
||||||
|
@ -179,7 +175,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||||
/// assert_eq!(path.get("tail").unwrap(), "main/LICENSE");
|
/// assert_eq!(path.get("tail").unwrap(), "main/LICENSE");
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
|
||||||
/// # Multi-Pattern Resources
|
/// # Multi-Pattern Resources
|
||||||
/// For resources that can map to multiple distinct paths, it may be suitable to use
|
/// For resources that can map to multiple distinct paths, it may be suitable to use
|
||||||
/// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined
|
/// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined
|
||||||
|
@ -198,7 +193,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||||
/// assert!(resource.is_match("/index"));
|
/// assert!(resource.is_match("/index"));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
///
|
|
||||||
/// # Trailing Slashes
|
/// # Trailing Slashes
|
||||||
/// It should be noted that this library takes no steps to normalize intra-path or trailing slashes.
|
/// It should be noted that this library takes no steps to normalize intra-path or trailing slashes.
|
||||||
/// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if
|
/// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if
|
||||||
|
@ -212,6 +206,8 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||||
/// assert!(!ResourceDef::new("/root/").is_match("/root"));
|
/// assert!(!ResourceDef::new("/root/").is_match("/root"));
|
||||||
/// assert!(!ResourceDef::prefix("/root/").is_match("/root"));
|
/// assert!(!ResourceDef::prefix("/root/").is_match("/root"));
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// [matching behavior section]: #pattern-format-and-matching-behavior
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ResourceDef {
|
pub struct ResourceDef {
|
||||||
id: u16,
|
id: u16,
|
||||||
|
@ -279,7 +275,7 @@ impl ResourceDef {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new<T: IntoPatterns>(paths: T) -> Self {
|
pub fn new<T: IntoPatterns>(paths: T) -> Self {
|
||||||
profile_method!(new);
|
profile_method!(new);
|
||||||
Self::new2(paths, false)
|
Self::construct(paths, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a new resource definition using a pattern that performs prefix matching.
|
/// Constructs a new resource definition using a pattern that performs prefix matching.
|
||||||
|
@ -292,7 +288,7 @@ impl ResourceDef {
|
||||||
/// resource definition with a tail segment; use [`new`][Self::new] in this case.
|
/// resource definition with a tail segment; use [`new`][Self::new] in this case.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if path regex pattern is malformed.
|
/// Panics if path pattern is malformed.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -307,14 +303,14 @@ impl ResourceDef {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn prefix<T: IntoPatterns>(paths: T) -> Self {
|
pub fn prefix<T: IntoPatterns>(paths: T) -> Self {
|
||||||
profile_method!(prefix);
|
profile_method!(prefix);
|
||||||
ResourceDef::new2(paths, true)
|
ResourceDef::construct(paths, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a new resource definition using a string pattern that performs prefix matching,
|
/// Constructs a new resource definition using a string pattern that performs prefix matching,
|
||||||
/// inserting a `/` to beginning of the pattern if absent and pattern is not empty.
|
/// ensuring a leading `/` if pattern is not empty.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if path regex pattern is malformed.
|
/// Panics if path pattern is malformed.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -515,8 +511,8 @@ impl ResourceDef {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
match patterns.len() {
|
match patterns.len() {
|
||||||
1 => ResourceDef::new2(&patterns[0], other.is_prefix()),
|
1 => ResourceDef::construct(&patterns[0], other.is_prefix()),
|
||||||
_ => ResourceDef::new2(patterns, other.is_prefix()),
|
_ => ResourceDef::construct(patterns, other.is_prefix()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -881,8 +877,8 @@ impl ResourceDef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new2<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
|
fn construct<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
|
||||||
profile_method!(new2);
|
profile_method!(construct);
|
||||||
|
|
||||||
let patterns = paths.patterns();
|
let patterns = paths.patterns();
|
||||||
let (pat_type, segments) = match &patterns {
|
let (pat_type, segments) = match &patterns {
|
||||||
|
@ -1814,7 +1810,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
fn prefix_plus_tail_match_is_allowed() {
|
fn prefix_plus_tail_match_disallowed() {
|
||||||
ResourceDef::prefix("/user/{id}*");
|
ResourceDef::prefix("/user/{id}*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 0.1.0-beta.10 - 2021-12-27
|
||||||
|
* No significant changes since `0.1.0-beta.9`.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.9 - 2021-12-17
|
## 0.1.0-beta.9 - 2021-12-17
|
||||||
- Re-export `actix_http::body::to_bytes`. [#2518]
|
- Re-export `actix_http::body::to_bytes`. [#2518]
|
||||||
- Update `actix_web::test` re-exports. [#2518]
|
- Update `actix_web::test` re-exports. [#2518]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-test"
|
name = "actix-test"
|
||||||
version = "0.1.0-beta.9"
|
version = "0.1.0-beta.10"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.4.1"
|
actix-codec = "0.4.1"
|
||||||
actix-http = "3.0.0-beta.16"
|
actix-http = "3.0.0-beta.17"
|
||||||
actix-http-test = "3.0.0-beta.9"
|
actix-http-test = "3.0.0-beta.10"
|
||||||
actix-rt = "2.1"
|
actix-rt = "2.1"
|
||||||
actix-service = "2.0.0"
|
actix-service = "2.0.0"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3.0.0"
|
||||||
actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] }
|
||||||
awc = { version = "3.0.0-beta.14", default-features = false, features = ["cookies"] }
|
awc = { version = "3.0.0-beta.15", default-features = false, features = ["cookies"] }
|
||||||
|
|
||||||
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||||
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
||||||
|
@ -45,4 +45,4 @@ serde_json = "1"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||||
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
||||||
tokio = { version = "1.2", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.9 - 2021-12-27
|
||||||
|
* No significant changes since `4.0.0-beta.8`.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.8 - 2021-12-11
|
## 4.0.0-beta.8 - 2021-12-11
|
||||||
- Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920]
|
- Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920]
|
||||||
- Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920]
|
- Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-web-actors"
|
name = "actix-web-actors"
|
||||||
version = "4.0.0-beta.8"
|
version = "4.0.0-beta.9"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Actix actors support for Actix Web"
|
description = "Actix actors support for Actix Web"
|
||||||
keywords = ["actix", "http", "web", "framework", "async"]
|
keywords = ["actix", "http", "web", "framework", "async"]
|
||||||
|
@ -16,19 +16,19 @@ path = "src/lib.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = { version = "0.12.0", default-features = false }
|
actix = { version = "0.12.0", default-features = false }
|
||||||
actix-codec = "0.4.1"
|
actix-codec = "0.4.1"
|
||||||
actix-http = "3.0.0-beta.16"
|
actix-http = "3.0.0-beta.17"
|
||||||
actix-web = { version = "4.0.0-beta.15", default-features = false }
|
actix-web = { version = "4.0.0-beta.16", default-features = false }
|
||||||
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
tokio = { version = "1", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-test = "0.1.0-beta.9"
|
actix-test = "0.1.0-beta.10"
|
||||||
awc = { version = "3.0.0-beta.14", default-features = false }
|
awc = { version = "3.0.0-beta.15", default-features = false }
|
||||||
|
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
> Actix actors support for Actix Web.
|
> Actix actors support for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web-actors)
|
[](https://crates.io/crates/actix-web-actors)
|
||||||
[](https://docs.rs/actix-web-actors/4.0.0-beta.8)
|
[](https://docs.rs/actix-web-actors/4.0.0-beta.9)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-web-actors/4.0.0-beta.8)
|
[](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9)
|
||||||
[](https://crates.io/crates/actix-web-actors)
|
[](https://crates.io/crates/actix-web-actors)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,9 @@ actix-router = "0.5.0-beta.3"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-macros = "0.2.3"
|
actix-macros = "0.2.3"
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-test = "0.1.0-beta.9"
|
actix-test = "0.1.0-beta.10"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3.0.0"
|
||||||
actix-web = "4.0.0-beta.15"
|
actix-web = "4.0.0-beta.16"
|
||||||
|
|
||||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
trybuild = "1"
|
trybuild = "1"
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
- `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553]
|
||||||
|
- `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553]
|
||||||
|
- Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553]
|
||||||
|
|
||||||
|
[#2553]: https://github.com/actix/actix-web/pull/2553
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.15 - 2021-12-27
|
||||||
- Rename `Connector::{ssl => openssl}`. [#2503]
|
- Rename `Connector::{ssl => openssl}`. [#2503]
|
||||||
- Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503]
|
- Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503]
|
||||||
|
- `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546]
|
||||||
|
- Rename `MessageBody => ResponseBody` to avoid conflicts with `MessageBody` trait. [#2546]
|
||||||
|
- `impl Future` for `ResponseBody` no longer requires the body type be `Unpin`. [#2546]
|
||||||
|
- `impl Future` for `JsonBody` no longer requires the body type be `Unpin`. [#2546]
|
||||||
|
- `impl Stream` for `ClientResponse` no longer requires the body type be `Unpin`. [#2546]
|
||||||
|
|
||||||
[#2503]: https://github.com/actix/actix-web/pull/2503
|
[#2503]: https://github.com/actix/actix-web/pull/2503
|
||||||
|
[#2546]: https://github.com/actix/actix-web/pull/2546
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.14 - 2021-12-17
|
## 3.0.0-beta.14 - 2021-12-17
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "awc"
|
name = "awc"
|
||||||
version = "3.0.0-beta.14"
|
version = "3.0.0-beta.15"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"fakeshadow <24548779@qq.com>",
|
"fakeshadow <24548779@qq.com>",
|
||||||
|
@ -60,9 +60,9 @@ dangerous-h2c = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.4.1"
|
actix-codec = "0.4.1"
|
||||||
actix-service = "2.0.0"
|
actix-service = "2.0.0"
|
||||||
actix-http = "3.0.0-beta.16"
|
actix-http = "3.0.0-beta.17"
|
||||||
actix-rt = { version = "2.1", default-features = false }
|
actix-rt = { version = "2.1", default-features = false }
|
||||||
actix-tls = { version = "3.0.0-rc.2", features = ["connect", "uri"] }
|
actix-tls = { version = "3.0.0", features = ["connect", "uri"] }
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3.0.0"
|
||||||
|
|
||||||
ahash = "0.7"
|
ahash = "0.7"
|
||||||
|
@ -74,7 +74,7 @@ futures-core = { version = "0.3.7", default-features = false, features = ["alloc
|
||||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||||
h2 = "0.3.9"
|
h2 = "0.3.9"
|
||||||
http = "0.2.5"
|
http = "0.2.5"
|
||||||
itoa = "0.4"
|
itoa = "1"
|
||||||
log =" 0.4"
|
log =" 0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
|
@ -83,7 +83,7 @@ rand = "0.8"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
tokio = { version = "1", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
|
|
||||||
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
|
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
|
||||||
|
|
||||||
|
@ -93,13 +93,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
|
||||||
trust-dns-resolver = { version = "0.20.0", optional = true }
|
trust-dns-resolver = { version = "0.20.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http = { version = "3.0.0-beta.16", features = ["openssl"] }
|
actix-http = { version = "3.0.0-beta.17", features = ["openssl"] }
|
||||||
actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] }
|
actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] }
|
||||||
actix-server = "2.0.0-rc.1"
|
actix-server = "2.0.0-rc.2"
|
||||||
actix-test = { version = "0.1.0-beta.9", features = ["openssl", "rustls"] }
|
actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] }
|
||||||
actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] }
|
actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] }
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3.0.0"
|
||||||
actix-web = { version = "4.0.0-beta.15", features = ["openssl"] }
|
actix-web = { version = "4.0.0-beta.16", features = ["openssl"] }
|
||||||
|
|
||||||
brotli2 = "0.3.2"
|
brotli2 = "0.3.2"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
> Async HTTP and WebSocket client library.
|
> Async HTTP and WebSocket client library.
|
||||||
|
|
||||||
[](https://crates.io/crates/awc)
|
[](https://crates.io/crates/awc)
|
||||||
[](https://docs.rs/awc/3.0.0-beta.14)
|
[](https://docs.rs/awc/3.0.0-beta.15)
|
||||||

|

|
||||||
[](https://deps.rs/crate/awc/3.0.0-beta.14)
|
[](https://deps.rs/crate/awc/3.0.0-beta.15)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
|
||||||
fmt, mem,
|
fmt, mem,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::Bytes;
|
||||||
use futures_core::Stream;
|
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream};
|
use actix_http::body::{BodySize, BoxBody, MessageBody};
|
||||||
|
|
||||||
use crate::BoxError;
|
|
||||||
|
|
||||||
pin_project! {
|
pin_project! {
|
||||||
/// Represents various types of HTTP message body.
|
/// Represents various types of HTTP message body.
|
||||||
|
@ -77,10 +73,27 @@ impl<B> AnyBody<B>
|
||||||
where
|
where
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
|
/// Converts a [`MessageBody`] type into the best possible representation.
|
||||||
|
///
|
||||||
|
/// Checks size for `None` and tries to convert to `Bytes`. Otherwise, uses the `Body` variant.
|
||||||
|
pub fn from_message_body(body: B) -> Self
|
||||||
|
where
|
||||||
|
B: MessageBody,
|
||||||
|
{
|
||||||
|
if matches!(body.size(), BodySize::None) {
|
||||||
|
return Self::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match body.try_into_bytes() {
|
||||||
|
Ok(body) => Self::Bytes { body },
|
||||||
|
Err(body) => Self::new(body),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn into_boxed(self) -> AnyBody {
|
pub fn into_boxed(self) -> AnyBody {
|
||||||
match self {
|
match self {
|
||||||
Self::None => AnyBody::None,
|
Self::None => AnyBody::None,
|
||||||
Self::Bytes { body: bytes } => AnyBody::Bytes { body: bytes },
|
Self::Bytes { body } => AnyBody::Bytes { body },
|
||||||
Self::Body { body } => AnyBody::new_boxed(body),
|
Self::Body { body } => AnyBody::new_boxed(body),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,91 +156,6 @@ impl<S: fmt::Debug> fmt::Debug for AnyBody<S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B> From<&'static str> for AnyBody<B> {
|
|
||||||
fn from(string: &'static str) -> Self {
|
|
||||||
Self::Bytes {
|
|
||||||
body: Bytes::from_static(string.as_ref()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> From<&'static [u8]> for AnyBody<B> {
|
|
||||||
fn from(bytes: &'static [u8]) -> Self {
|
|
||||||
Self::Bytes {
|
|
||||||
body: Bytes::from_static(bytes),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> From<Vec<u8>> for AnyBody<B> {
|
|
||||||
fn from(vec: Vec<u8>) -> Self {
|
|
||||||
Self::Bytes {
|
|
||||||
body: Bytes::from(vec),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> From<String> for AnyBody<B> {
|
|
||||||
fn from(string: String) -> Self {
|
|
||||||
Self::Bytes {
|
|
||||||
body: Bytes::from(string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> From<&'_ String> for AnyBody<B> {
|
|
||||||
fn from(string: &String) -> Self {
|
|
||||||
Self::Bytes {
|
|
||||||
body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> From<Cow<'_, str>> for AnyBody<B> {
|
|
||||||
fn from(string: Cow<'_, str>) -> Self {
|
|
||||||
match string {
|
|
||||||
Cow::Owned(s) => Self::from(s),
|
|
||||||
Cow::Borrowed(s) => Self::Bytes {
|
|
||||||
body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> From<Bytes> for AnyBody<B> {
|
|
||||||
fn from(bytes: Bytes) -> Self {
|
|
||||||
Self::Bytes { body: bytes }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> From<BytesMut> for AnyBody<B> {
|
|
||||||
fn from(bytes: BytesMut) -> Self {
|
|
||||||
Self::Bytes {
|
|
||||||
body: bytes.freeze(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> From<SizedStream<S>> for AnyBody
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
|
||||||
E: Into<BoxError> + 'static,
|
|
||||||
{
|
|
||||||
fn from(stream: SizedStream<S>) -> Self {
|
|
||||||
AnyBody::new_boxed(stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> From<BodyStream<S>> for AnyBody
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
|
||||||
E: Into<BoxError> + 'static,
|
|
||||||
{
|
|
||||||
fn from(stream: BodyStream<S>) -> Self {
|
|
||||||
AnyBody::new_boxed(stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::marker::PhantomPinned;
|
use std::marker::PhantomPinned;
|
||||||
|
|
|
@ -9,11 +9,13 @@ use actix_rt::net::{ActixStream, TcpStream};
|
||||||
use actix_service::{boxed, Service};
|
use actix_service::{boxed, Service};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection},
|
client::{
|
||||||
|
ClientConfig, ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection,
|
||||||
|
},
|
||||||
connect::DefaultConnector,
|
connect::DefaultConnector,
|
||||||
error::SendRequestError,
|
error::SendRequestError,
|
||||||
middleware::{NestTransform, Redirect, Transform},
|
middleware::{NestTransform, Redirect, Transform},
|
||||||
Client, ClientConfig, ConnectRequest, ConnectResponse,
|
Client, ConnectRequest, ConnectResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An HTTP Client builder
|
/// An HTTP Client builder
|
||||||
|
|
|
@ -267,7 +267,9 @@ where
|
||||||
Connection::Tls(ConnectionType::H2(conn)) => {
|
Connection::Tls(ConnectionType::H2(conn)) => {
|
||||||
h2proto::send_request(conn, head.into(), body).await
|
h2proto::send_request(conn, head.into(), body).await
|
||||||
}
|
}
|
||||||
_ => unreachable!("Plain Tcp connection can be used only in Http1 protocol"),
|
_ => {
|
||||||
|
unreachable!("Plain TCP connection can be used only with HTTP/1.1 protocol")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,17 @@ use actix_http::{
|
||||||
Payload, RequestHeadType, ResponseHead, StatusCode,
|
Payload, RequestHeadType, ResponseHead, StatusCode,
|
||||||
};
|
};
|
||||||
use actix_utils::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
use bytes::buf::BufMut;
|
use bytes::{buf::BufMut, Bytes, BytesMut};
|
||||||
use bytes::{Bytes, BytesMut};
|
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
use futures_util::SinkExt as _;
|
use futures_util::SinkExt as _;
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
|
|
||||||
use super::connection::{ConnectionIo, H1Connection};
|
use super::{
|
||||||
use super::error::{ConnectError, SendRequestError};
|
connection::{ConnectionIo, H1Connection},
|
||||||
|
error::{ConnectError, SendRequestError},
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) async fn send_request<Io, B>(
|
pub(crate) async fn send_request<Io, B>(
|
||||||
io: H1Connection<Io>,
|
io: H1Connection<Io>,
|
||||||
|
@ -123,7 +124,12 @@ where
|
||||||
|
|
||||||
Ok((head, Payload::None))
|
Ok((head, Payload::None))
|
||||||
}
|
}
|
||||||
_ => Ok((head, Payload::Stream(Box::pin(PlStream::new(framed))))),
|
_ => Ok((
|
||||||
|
head,
|
||||||
|
Payload::Stream {
|
||||||
|
payload: Box::pin(PlStream::new(framed)),
|
||||||
|
},
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,9 +52,11 @@ where
|
||||||
let _ = match length {
|
let _ = match length {
|
||||||
BodySize::None => None,
|
BodySize::None => None,
|
||||||
|
|
||||||
BodySize::Sized(0) => req
|
BodySize::Sized(0) => {
|
||||||
.headers_mut()
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
|
const HV_ZERO: HeaderValue = HeaderValue::from_static("0");
|
||||||
|
req.headers_mut().insert(CONTENT_LENGTH, HV_ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
BodySize::Sized(len) => {
|
BodySize::Sized(len) => {
|
||||||
let mut buf = itoa::Buffer::new();
|
let mut buf = itoa::Buffer::new();
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
//! HTTP client.
|
//! HTTP client.
|
||||||
|
|
||||||
use http::Uri;
|
use std::{convert::TryFrom, rc::Rc, time::Duration};
|
||||||
|
|
||||||
|
use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri};
|
||||||
|
use actix_rt::net::TcpStream;
|
||||||
|
use actix_service::Service;
|
||||||
|
pub use actix_tls::connect::{
|
||||||
|
ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{ws, BoxConnectorService, ClientBuilder, ClientRequest};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod connection;
|
mod connection;
|
||||||
|
@ -10,10 +19,6 @@ mod h1proto;
|
||||||
mod h2proto;
|
mod h2proto;
|
||||||
mod pool;
|
mod pool;
|
||||||
|
|
||||||
pub use actix_tls::connect::{
|
|
||||||
ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use self::connection::{Connection, ConnectionIo};
|
pub use self::connection::{Connection, ConnectionIo};
|
||||||
pub use self::connector::{Connector, ConnectorService};
|
pub use self::connector::{Connector, ConnectorService};
|
||||||
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
|
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
|
||||||
|
@ -23,3 +28,176 @@ pub struct Connect {
|
||||||
pub uri: Uri,
|
pub uri: Uri,
|
||||||
pub addr: Option<std::net::SocketAddr>,
|
pub addr: Option<std::net::SocketAddr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An asynchronous HTTP and WebSocket client.
|
||||||
|
///
|
||||||
|
/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU
|
||||||
|
/// and memory usage.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use awc::Client;
|
||||||
|
///
|
||||||
|
/// #[actix_rt::main]
|
||||||
|
/// async fn main() {
|
||||||
|
/// let mut client = Client::default();
|
||||||
|
///
|
||||||
|
/// let res = client.get("http://www.rust-lang.org")
|
||||||
|
/// .insert_header(("User-Agent", "my-app/1.2"))
|
||||||
|
/// .send()
|
||||||
|
/// .await;
|
||||||
|
///
|
||||||
|
/// println!("Response: {:?}", res);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Client(pub(crate) ClientConfig);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct ClientConfig {
|
||||||
|
pub(crate) connector: BoxConnectorService,
|
||||||
|
pub(crate) default_headers: Rc<HeaderMap>,
|
||||||
|
pub(crate) timeout: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Client {
|
||||||
|
fn default() -> Self {
|
||||||
|
ClientBuilder::new().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
/// Create new client instance with default settings.
|
||||||
|
pub fn new() -> Client {
|
||||||
|
Client::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create `Client` builder.
|
||||||
|
/// This function is equivalent of `ClientBuilder::new()`.
|
||||||
|
pub fn builder() -> ClientBuilder<
|
||||||
|
impl Service<
|
||||||
|
ConnectInfo<Uri>,
|
||||||
|
Response = TcpConnection<Uri, TcpStream>,
|
||||||
|
Error = TcpConnectError,
|
||||||
|
> + Clone,
|
||||||
|
> {
|
||||||
|
ClientBuilder::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct HTTP request.
|
||||||
|
pub fn request<U>(&self, method: Method, url: U) -> ClientRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
let mut req = ClientRequest::new(method, url, self.0.clone());
|
||||||
|
|
||||||
|
for header in self.0.default_headers.iter() {
|
||||||
|
// header map is empty
|
||||||
|
// TODO: probably append instead
|
||||||
|
req = req.insert_header_if_none(header);
|
||||||
|
}
|
||||||
|
req
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create `ClientRequest` from `RequestHead`
|
||||||
|
///
|
||||||
|
/// It is useful for proxy requests. This implementation
|
||||||
|
/// copies all headers and the method.
|
||||||
|
pub fn request_from<U>(&self, url: U, head: &RequestHead) -> ClientRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
let mut req = self.request(head.method.clone(), url);
|
||||||
|
for header in head.headers.iter() {
|
||||||
|
req = req.insert_header_if_none(header);
|
||||||
|
}
|
||||||
|
req
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct HTTP *GET* request.
|
||||||
|
pub fn get<U>(&self, url: U) -> ClientRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
self.request(Method::GET, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct HTTP *HEAD* request.
|
||||||
|
pub fn head<U>(&self, url: U) -> ClientRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
self.request(Method::HEAD, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct HTTP *PUT* request.
|
||||||
|
pub fn put<U>(&self, url: U) -> ClientRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
self.request(Method::PUT, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct HTTP *POST* request.
|
||||||
|
pub fn post<U>(&self, url: U) -> ClientRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
self.request(Method::POST, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct HTTP *PATCH* request.
|
||||||
|
pub fn patch<U>(&self, url: U) -> ClientRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
self.request(Method::PATCH, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct HTTP *DELETE* request.
|
||||||
|
pub fn delete<U>(&self, url: U) -> ClientRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
self.request(Method::DELETE, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct HTTP *OPTIONS* request.
|
||||||
|
pub fn options<U>(&self, url: U) -> ClientRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
self.request(Method::OPTIONS, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a WebSocket connection.
|
||||||
|
/// Returns a WebSocket connection builder.
|
||||||
|
pub fn ws<U>(&self, url: U) -> ws::WebsocketsRequest
|
||||||
|
where
|
||||||
|
Uri: TryFrom<U>,
|
||||||
|
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||||
|
{
|
||||||
|
let mut req = ws::WebsocketsRequest::new(url, self.0.clone());
|
||||||
|
for (key, value) in self.0.default_headers.iter() {
|
||||||
|
req.head.headers.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
req
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 headers(&mut self) -> Option<&mut HeaderMap> {
|
||||||
|
Rc::get_mut(&mut self.0.default_headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::{
|
||||||
client::{
|
client::{
|
||||||
Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError,
|
Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError,
|
||||||
},
|
},
|
||||||
response::ClientResponse,
|
ClientResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type BoxConnectorService = Rc<
|
pub type BoxConnectorService = Rc<
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//! HTTP client errors
|
//! HTTP client errors
|
||||||
|
|
||||||
|
// TODO: figure out how best to expose http::Error vs actix_http::Error
|
||||||
pub use actix_http::{
|
pub use actix_http::{
|
||||||
error::{HttpError, PayloadError},
|
error::{HttpError, PayloadError},
|
||||||
header::HeaderValue,
|
header::HeaderValue,
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
use std::{convert::TryFrom, net, rc::Rc, time::Duration};
|
use std::{net, rc::Rc, time::Duration};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
|
body::MessageBody,
|
||||||
error::HttpError,
|
error::HttpError,
|
||||||
header::{HeaderMap, HeaderName, TryIntoHeaderValue},
|
header::{HeaderMap, TryIntoHeaderPair},
|
||||||
Method, RequestHead, Uri,
|
Method, RequestHead, Uri,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any_body::AnyBody,
|
client::ClientConfig,
|
||||||
sender::{RequestSender, SendClientRequest},
|
sender::{RequestSender, SendClientRequest},
|
||||||
BoxError, ClientConfig,
|
BoxError,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// `FrozenClientRequest` struct represents cloneable client request.
|
/// `FrozenClientRequest` struct represents cloneable client request.
|
||||||
|
///
|
||||||
/// It could be used to send same request multiple times.
|
/// It could be used to send same request multiple times.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FrozenClientRequest {
|
pub struct FrozenClientRequest {
|
||||||
|
@ -46,7 +48,7 @@ impl FrozenClientRequest {
|
||||||
/// Send a body.
|
/// Send a body.
|
||||||
pub fn send_body<B>(&self, body: B) -> SendClientRequest
|
pub fn send_body<B>(&self, body: B) -> SendClientRequest
|
||||||
where
|
where
|
||||||
B: Into<AnyBody>,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
RequestSender::Rc(self.head.clone(), None).send_body(
|
RequestSender::Rc(self.head.clone(), None).send_body(
|
||||||
self.addr,
|
self.addr,
|
||||||
|
@ -82,7 +84,7 @@ impl FrozenClientRequest {
|
||||||
/// Send a streaming body.
|
/// Send a streaming body.
|
||||||
pub fn send_stream<S, E>(&self, stream: S) -> SendClientRequest
|
pub fn send_stream<S, E>(&self, stream: S) -> SendClientRequest
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||||
E: Into<BoxError> + 'static,
|
E: Into<BoxError> + 'static,
|
||||||
{
|
{
|
||||||
RequestSender::Rc(self.head.clone(), None).send_stream(
|
RequestSender::Rc(self.head.clone(), None).send_stream(
|
||||||
|
@ -104,20 +106,14 @@ impl FrozenClientRequest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a `FrozenSendBuilder` with extra headers
|
/// Clones this `FrozenClientRequest`, returning a new one with extra headers added.
|
||||||
pub fn extra_headers(&self, extra_headers: HeaderMap) -> FrozenSendBuilder {
|
pub fn extra_headers(&self, extra_headers: HeaderMap) -> FrozenSendBuilder {
|
||||||
FrozenSendBuilder::new(self.clone(), extra_headers)
|
FrozenSendBuilder::new(self.clone(), extra_headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a `FrozenSendBuilder` with an extra header
|
/// Clones this `FrozenClientRequest`, returning a new one with the extra header added.
|
||||||
pub fn extra_header<K, V>(&self, key: K, value: V) -> FrozenSendBuilder
|
pub fn extra_header(&self, header: impl TryIntoHeaderPair) -> FrozenSendBuilder {
|
||||||
where
|
self.extra_headers(HeaderMap::new()).extra_header(header)
|
||||||
HeaderName: TryFrom<K>,
|
|
||||||
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
|
|
||||||
V: TryIntoHeaderValue,
|
|
||||||
{
|
|
||||||
self.extra_headers(HeaderMap::new())
|
|
||||||
.extra_header(key, value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,29 +134,20 @@ impl FrozenSendBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a header, it overrides existing header in `FrozenClientRequest`.
|
/// Insert a header, it overrides existing header in `FrozenClientRequest`.
|
||||||
pub fn extra_header<K, V>(mut self, key: K, value: V) -> Self
|
pub fn extra_header(mut self, header: impl TryIntoHeaderPair) -> Self {
|
||||||
where
|
match header.try_into_pair() {
|
||||||
HeaderName: TryFrom<K>,
|
Ok((key, value)) => {
|
||||||
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
|
self.extra_headers.insert(key, value);
|
||||||
V: TryIntoHeaderValue,
|
}
|
||||||
{
|
|
||||||
match HeaderName::try_from(key) {
|
Err(err) => self.err = Some(err.into()),
|
||||||
Ok(key) => match value.try_into_value() {
|
|
||||||
Ok(value) => {
|
|
||||||
self.extra_headers.insert(key, value);
|
|
||||||
}
|
|
||||||
Err(e) => self.err = Some(e.into()),
|
|
||||||
},
|
|
||||||
Err(e) => self.err = Some(e.into()),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Complete request construction and send a body.
|
/// Complete request construction and send a body.
|
||||||
pub fn send_body<B>(self, body: B) -> SendClientRequest
|
pub fn send_body(self, body: impl MessageBody + 'static) -> SendClientRequest {
|
||||||
where
|
|
||||||
B: Into<AnyBody>,
|
|
||||||
{
|
|
||||||
if let Some(e) = self.err {
|
if let Some(e) = self.err {
|
||||||
return e.into();
|
return e.into();
|
||||||
}
|
}
|
||||||
|
@ -175,9 +162,9 @@ impl FrozenSendBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Complete request construction and send a json body.
|
/// Complete request construction and send a json body.
|
||||||
pub fn send_json<T: Serialize>(self, value: &T) -> SendClientRequest {
|
pub fn send_json(self, value: impl Serialize) -> SendClientRequest {
|
||||||
if let Some(e) = self.err {
|
if let Some(err) = self.err {
|
||||||
return e.into();
|
return err.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_json(
|
RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_json(
|
||||||
|
@ -190,7 +177,7 @@ impl FrozenSendBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Complete request construction and send an urlencoded body.
|
/// Complete request construction and send an urlencoded body.
|
||||||
pub fn send_form<T: Serialize>(self, value: &T) -> SendClientRequest {
|
pub fn send_form(self, value: impl Serialize) -> SendClientRequest {
|
||||||
if let Some(e) = self.err {
|
if let Some(e) = self.err {
|
||||||
return e.into();
|
return e.into();
|
||||||
}
|
}
|
||||||
|
@ -207,7 +194,7 @@ impl FrozenSendBuilder {
|
||||||
/// Complete request construction and send a streaming body.
|
/// Complete request construction and send a streaming body.
|
||||||
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
|
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||||
E: Into<BoxError> + 'static,
|
E: Into<BoxError> + 'static,
|
||||||
{
|
{
|
||||||
if let Some(e) = self.err {
|
if let Some(e) = self.err {
|
||||||
|
|
205
awc/src/lib.rs
205
awc/src/lib.rs
|
@ -105,6 +105,11 @@
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
|
||||||
|
pub use actix_http::body;
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
pub use cookie;
|
||||||
|
|
||||||
mod any_body;
|
mod any_body;
|
||||||
mod builder;
|
mod builder;
|
||||||
mod client;
|
mod client;
|
||||||
|
@ -113,203 +118,27 @@ pub mod error;
|
||||||
mod frozen;
|
mod frozen;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
mod request;
|
mod request;
|
||||||
mod response;
|
mod responses;
|
||||||
mod sender;
|
mod sender;
|
||||||
pub mod test;
|
pub mod test;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
// TODO: hmmmmmm
|
pub mod http {
|
||||||
pub use actix_http as http;
|
//! Various HTTP related types.
|
||||||
#[cfg(feature = "cookies")]
|
|
||||||
pub use cookie;
|
// TODO: figure out how best to expose http::Error vs actix_http::Error
|
||||||
|
pub use actix_http::{
|
||||||
|
header, uri, ConnectionType, Error, Method, StatusCode, Uri, Version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub use self::builder::ClientBuilder;
|
pub use self::builder::ClientBuilder;
|
||||||
pub use self::client::Connector;
|
pub use self::client::{Client, Connector};
|
||||||
pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse};
|
pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse};
|
||||||
pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder};
|
pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder};
|
||||||
pub use self::request::ClientRequest;
|
pub use self::request::ClientRequest;
|
||||||
pub use self::response::{ClientResponse, JsonBody, MessageBody};
|
#[allow(deprecated)]
|
||||||
|
pub use self::responses::{ClientResponse, JsonBody, MessageBody, ResponseBody};
|
||||||
pub use self::sender::SendClientRequest;
|
pub use self::sender::SendClientRequest;
|
||||||
|
|
||||||
use std::{convert::TryFrom, rc::Rc, time::Duration};
|
|
||||||
|
|
||||||
use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri};
|
|
||||||
use actix_rt::net::TcpStream;
|
|
||||||
use actix_service::Service;
|
|
||||||
|
|
||||||
use self::client::{ConnectInfo, TcpConnectError, TcpConnection};
|
|
||||||
|
|
||||||
pub(crate) type BoxError = Box<dyn std::error::Error>;
|
pub(crate) type BoxError = Box<dyn std::error::Error>;
|
||||||
|
|
||||||
/// An asynchronous HTTP and WebSocket client.
|
|
||||||
///
|
|
||||||
/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU
|
|
||||||
/// and memory usage.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// use awc::Client;
|
|
||||||
///
|
|
||||||
/// #[actix_rt::main]
|
|
||||||
/// async fn main() {
|
|
||||||
/// let mut client = Client::default();
|
|
||||||
///
|
|
||||||
/// let res = client.get("http://www.rust-lang.org")
|
|
||||||
/// .insert_header(("User-Agent", "my-app/1.2"))
|
|
||||||
/// .send()
|
|
||||||
/// .await;
|
|
||||||
///
|
|
||||||
/// println!("Response: {:?}", res);
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Client(ClientConfig);
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct ClientConfig {
|
|
||||||
pub(crate) connector: BoxConnectorService,
|
|
||||||
pub(crate) default_headers: Rc<HeaderMap>,
|
|
||||||
pub(crate) timeout: Option<Duration>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Client {
|
|
||||||
fn default() -> Self {
|
|
||||||
ClientBuilder::new().finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
/// Create new client instance with default settings.
|
|
||||||
pub fn new() -> Client {
|
|
||||||
Client::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create `Client` builder.
|
|
||||||
/// This function is equivalent of `ClientBuilder::new()`.
|
|
||||||
pub fn builder() -> ClientBuilder<
|
|
||||||
impl Service<
|
|
||||||
ConnectInfo<Uri>,
|
|
||||||
Response = TcpConnection<Uri, TcpStream>,
|
|
||||||
Error = TcpConnectError,
|
|
||||||
> + Clone,
|
|
||||||
> {
|
|
||||||
ClientBuilder::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct HTTP request.
|
|
||||||
pub fn request<U>(&self, method: Method, url: U) -> ClientRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
let mut req = ClientRequest::new(method, url, self.0.clone());
|
|
||||||
|
|
||||||
for header in self.0.default_headers.iter() {
|
|
||||||
// header map is empty
|
|
||||||
// TODO: probably append instead
|
|
||||||
req = req.insert_header_if_none(header);
|
|
||||||
}
|
|
||||||
req
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create `ClientRequest` from `RequestHead`
|
|
||||||
///
|
|
||||||
/// It is useful for proxy requests. This implementation
|
|
||||||
/// copies all headers and the method.
|
|
||||||
pub fn request_from<U>(&self, url: U, head: &RequestHead) -> ClientRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
let mut req = self.request(head.method.clone(), url);
|
|
||||||
for header in head.headers.iter() {
|
|
||||||
req = req.insert_header_if_none(header);
|
|
||||||
}
|
|
||||||
req
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct HTTP *GET* request.
|
|
||||||
pub fn get<U>(&self, url: U) -> ClientRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
self.request(Method::GET, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct HTTP *HEAD* request.
|
|
||||||
pub fn head<U>(&self, url: U) -> ClientRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
self.request(Method::HEAD, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct HTTP *PUT* request.
|
|
||||||
pub fn put<U>(&self, url: U) -> ClientRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
self.request(Method::PUT, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct HTTP *POST* request.
|
|
||||||
pub fn post<U>(&self, url: U) -> ClientRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
self.request(Method::POST, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct HTTP *PATCH* request.
|
|
||||||
pub fn patch<U>(&self, url: U) -> ClientRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
self.request(Method::PATCH, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct HTTP *DELETE* request.
|
|
||||||
pub fn delete<U>(&self, url: U) -> ClientRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
self.request(Method::DELETE, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct HTTP *OPTIONS* request.
|
|
||||||
pub fn options<U>(&self, url: U) -> ClientRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
self.request(Method::OPTIONS, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize a WebSocket connection.
|
|
||||||
/// Returns a WebSocket connection builder.
|
|
||||||
pub fn ws<U>(&self, url: U) -> ws::WebsocketsRequest
|
|
||||||
where
|
|
||||||
Uri: TryFrom<U>,
|
|
||||||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
|
||||||
{
|
|
||||||
let mut req = ws::WebsocketsRequest::new(url, self.0.clone());
|
|
||||||
for (key, value) in self.0.default_headers.iter() {
|
|
||||||
req.head.headers.insert(key.clone(), value.clone());
|
|
||||||
}
|
|
||||||
req
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 headers(&mut self) -> Option<&mut HeaderMap> {
|
|
||||||
Rc::get_mut(&mut self.0.default_headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,17 +5,18 @@ use futures_core::Stream;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
|
body::MessageBody,
|
||||||
error::HttpError,
|
error::HttpError,
|
||||||
header::{self, HeaderMap, HeaderValue, TryIntoHeaderPair},
|
header::{self, HeaderMap, HeaderValue, TryIntoHeaderPair},
|
||||||
ConnectionType, Method, RequestHead, Uri, Version,
|
ConnectionType, Method, RequestHead, Uri, Version,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any_body::AnyBody,
|
client::ClientConfig,
|
||||||
error::{FreezeRequestError, InvalidUrl},
|
error::{FreezeRequestError, InvalidUrl},
|
||||||
frozen::FrozenClientRequest,
|
frozen::FrozenClientRequest,
|
||||||
sender::{PrepForSendingError, RequestSender, SendClientRequest},
|
sender::{PrepForSendingError, RequestSender, SendClientRequest},
|
||||||
BoxError, ClientConfig,
|
BoxError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
|
@ -26,20 +27,20 @@ use crate::cookie::{Cookie, CookieJar};
|
||||||
/// This type can be used to construct an instance of `ClientRequest` through a
|
/// This type can be used to construct an instance of `ClientRequest` through a
|
||||||
/// builder-like pattern.
|
/// builder-like pattern.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```no_run
|
||||||
/// #[actix_rt::main]
|
/// # #[actix_rt::main]
|
||||||
/// async fn main() {
|
/// # async fn main() {
|
||||||
/// let response = awc::Client::new()
|
/// let response = awc::Client::new()
|
||||||
/// .get("http://www.rust-lang.org") // <- Create request builder
|
/// .get("http://www.rust-lang.org") // <- Create request builder
|
||||||
/// .insert_header(("User-Agent", "Actix-web"))
|
/// .insert_header(("User-Agent", "Actix-web"))
|
||||||
/// .send() // <- Send HTTP request
|
/// .send() // <- Send HTTP request
|
||||||
/// .await;
|
/// .await;
|
||||||
///
|
///
|
||||||
/// response.and_then(|response| { // <- server HTTP response
|
/// response.and_then(|response| { // <- server HTTP response
|
||||||
/// println!("Response: {:?}", response);
|
/// println!("Response: {:?}", response);
|
||||||
/// Ok(())
|
/// Ok(())
|
||||||
/// });
|
/// });
|
||||||
/// }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct ClientRequest {
|
pub struct ClientRequest {
|
||||||
pub(crate) head: RequestHead,
|
pub(crate) head: RequestHead,
|
||||||
|
@ -174,17 +175,13 @@ impl ClientRequest {
|
||||||
|
|
||||||
/// Append a header, keeping any that were set with an equivalent field name.
|
/// Append a header, keeping any that were set with an equivalent field name.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```no_run
|
||||||
/// # #[actix_rt::main]
|
/// use awc::{http::header, Client};
|
||||||
/// # async fn main() {
|
|
||||||
/// # use awc::Client;
|
|
||||||
/// use awc::http::header::CONTENT_TYPE;
|
|
||||||
///
|
///
|
||||||
/// Client::new()
|
/// Client::new()
|
||||||
/// .get("http://www.rust-lang.org")
|
/// .get("http://www.rust-lang.org")
|
||||||
/// .insert_header(("X-TEST", "value"))
|
/// .insert_header(("X-TEST", "value"))
|
||||||
/// .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON));
|
/// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON));
|
||||||
/// # }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
|
pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
|
||||||
match header.try_into_pair() {
|
match header.try_into_pair() {
|
||||||
|
@ -252,23 +249,18 @@ impl ClientRequest {
|
||||||
|
|
||||||
/// Set a cookie
|
/// Set a cookie
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```no_run
|
||||||
/// #[actix_rt::main]
|
/// use awc::{cookie::Cookie, Client};
|
||||||
/// async fn main() {
|
|
||||||
/// let resp = awc::Client::new().get("https://www.rust-lang.org")
|
|
||||||
/// .cookie(
|
|
||||||
/// awc::cookie::Cookie::build("name", "value")
|
|
||||||
/// .domain("www.rust-lang.org")
|
|
||||||
/// .path("/")
|
|
||||||
/// .secure(true)
|
|
||||||
/// .http_only(true)
|
|
||||||
/// .finish(),
|
|
||||||
/// )
|
|
||||||
/// .send()
|
|
||||||
/// .await;
|
|
||||||
///
|
///
|
||||||
/// println!("Response: {:?}", resp);
|
/// # #[actix_rt::main]
|
||||||
/// }
|
/// # async fn main() {
|
||||||
|
/// let res = Client::new().get("https://httpbin.org/cookies")
|
||||||
|
/// .cookie(Cookie::new("name", "value"))
|
||||||
|
/// .send()
|
||||||
|
/// .await;
|
||||||
|
///
|
||||||
|
/// println!("Response: {:?}", res);
|
||||||
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
||||||
|
@ -340,7 +332,7 @@ impl ClientRequest {
|
||||||
/// Complete request construction and send body.
|
/// Complete request construction and send body.
|
||||||
pub fn send_body<B>(self, body: B) -> SendClientRequest
|
pub fn send_body<B>(self, body: B) -> SendClientRequest
|
||||||
where
|
where
|
||||||
B: Into<AnyBody>,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
let slf = match self.prep_for_sending() {
|
let slf = match self.prep_for_sending() {
|
||||||
Ok(slf) => slf,
|
Ok(slf) => slf,
|
||||||
|
@ -393,7 +385,7 @@ impl ClientRequest {
|
||||||
/// Set an streaming body and generate `ClientRequest`.
|
/// Set an streaming body and generate `ClientRequest`.
|
||||||
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
|
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||||
E: Into<BoxError> + 'static,
|
E: Into<BoxError> + 'static,
|
||||||
{
|
{
|
||||||
let slf = match self.prep_for_sending() {
|
let slf = match self.prep_for_sending() {
|
||||||
|
|
|
@ -1,556 +0,0 @@
|
||||||
use std::{
|
|
||||||
cell::{Ref, RefMut},
|
|
||||||
fmt,
|
|
||||||
future::Future,
|
|
||||||
io,
|
|
||||||
marker::PhantomData,
|
|
||||||
pin::Pin,
|
|
||||||
task::{Context, Poll},
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use actix_http::{
|
|
||||||
error::PayloadError, header, header::HeaderMap, Extensions, HttpMessage, Payload,
|
|
||||||
PayloadStream, ResponseHead, StatusCode, Version,
|
|
||||||
};
|
|
||||||
use actix_rt::time::{sleep, Sleep};
|
|
||||||
use bytes::{Bytes, BytesMut};
|
|
||||||
use futures_core::{ready, Stream};
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
|
|
||||||
#[cfg(feature = "cookies")]
|
|
||||||
use crate::cookie::{Cookie, ParseError as CookieParseError};
|
|
||||||
use crate::error::JsonPayloadError;
|
|
||||||
|
|
||||||
/// Client Response
|
|
||||||
pub struct ClientResponse<S = PayloadStream> {
|
|
||||||
pub(crate) head: ResponseHead,
|
|
||||||
pub(crate) payload: Payload<S>,
|
|
||||||
pub(crate) timeout: ResponseTimeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// helper enum with reusable sleep passed from `SendClientResponse`.
|
|
||||||
/// See `ClientResponse::_timeout` for reason.
|
|
||||||
pub(crate) enum ResponseTimeout {
|
|
||||||
Disabled(Option<Pin<Box<Sleep>>>),
|
|
||||||
Enabled(Pin<Box<Sleep>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ResponseTimeout {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Disabled(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseTimeout {
|
|
||||||
fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> {
|
|
||||||
match *self {
|
|
||||||
Self::Enabled(ref mut timeout) => {
|
|
||||||
if timeout.as_mut().poll(cx).is_ready() {
|
|
||||||
Err(PayloadError::Io(io::Error::new(
|
|
||||||
io::ErrorKind::TimedOut,
|
|
||||||
"Response Payload IO timed out",
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Disabled(_) => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> HttpMessage for ClientResponse<S> {
|
|
||||||
type Stream = S;
|
|
||||||
|
|
||||||
fn headers(&self) -> &HeaderMap {
|
|
||||||
&self.head.headers
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_payload(&mut self) -> Payload<S> {
|
|
||||||
std::mem::replace(&mut self.payload, Payload::None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extensions(&self) -> Ref<'_, Extensions> {
|
|
||||||
self.head.extensions()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
|
|
||||||
self.head.extensions_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> ClientResponse<S> {
|
|
||||||
/// Create new Request instance
|
|
||||||
pub(crate) fn new(head: ResponseHead, payload: Payload<S>) -> Self {
|
|
||||||
ClientResponse {
|
|
||||||
head,
|
|
||||||
payload,
|
|
||||||
timeout: ResponseTimeout::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn head(&self) -> &ResponseHead {
|
|
||||||
&self.head
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the Request Version.
|
|
||||||
#[inline]
|
|
||||||
pub fn version(&self) -> Version {
|
|
||||||
self.head().version
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the status from the server.
|
|
||||||
#[inline]
|
|
||||||
pub fn status(&self) -> StatusCode {
|
|
||||||
self.head().status
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Returns request's headers.
|
|
||||||
pub fn headers(&self) -> &HeaderMap {
|
|
||||||
&self.head().headers
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a body and return previous body value
|
|
||||||
pub fn map_body<F, U>(mut self, f: F) -> ClientResponse<U>
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut ResponseHead, Payload<S>) -> Payload<U>,
|
|
||||||
{
|
|
||||||
let payload = f(&mut self.head, self.payload);
|
|
||||||
|
|
||||||
ClientResponse {
|
|
||||||
payload,
|
|
||||||
head: self.head,
|
|
||||||
timeout: self.timeout,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a timeout duration for [`ClientResponse`](self::ClientResponse).
|
|
||||||
///
|
|
||||||
/// This duration covers the duration of processing the response body stream
|
|
||||||
/// and would end it as timeout error when deadline met.
|
|
||||||
///
|
|
||||||
/// Disabled by default.
|
|
||||||
pub fn timeout(self, dur: Duration) -> Self {
|
|
||||||
let timeout = match self.timeout {
|
|
||||||
ResponseTimeout::Disabled(Some(mut timeout))
|
|
||||||
| ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) {
|
|
||||||
Some(deadline) => {
|
|
||||||
timeout.as_mut().reset(deadline.into());
|
|
||||||
ResponseTimeout::Enabled(timeout)
|
|
||||||
}
|
|
||||||
None => ResponseTimeout::Enabled(Box::pin(sleep(dur))),
|
|
||||||
},
|
|
||||||
_ => ResponseTimeout::Enabled(Box::pin(sleep(dur))),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
payload: self.payload,
|
|
||||||
head: self.head,
|
|
||||||
timeout,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method does not enable timeout. It's used to pass the boxed `Sleep` from
|
|
||||||
/// `SendClientRequest` and reuse it's heap allocation together with it's slot in
|
|
||||||
/// timer wheel.
|
|
||||||
pub(crate) fn _timeout(mut self, timeout: Option<Pin<Box<Sleep>>>) -> Self {
|
|
||||||
self.timeout = ResponseTimeout::Disabled(timeout);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load request cookies.
|
|
||||||
#[cfg(feature = "cookies")]
|
|
||||||
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
|
||||||
struct Cookies(Vec<Cookie<'static>>);
|
|
||||||
|
|
||||||
if self.extensions().get::<Cookies>().is_none() {
|
|
||||||
let mut cookies = Vec::new();
|
|
||||||
for hdr in self.headers().get_all(&header::SET_COOKIE) {
|
|
||||||
let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
|
|
||||||
cookies.push(Cookie::parse_encoded(s)?.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")]
|
|
||||||
pub 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<S> ClientResponse<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>>,
|
|
||||||
{
|
|
||||||
/// Loads HTTP response's body.
|
|
||||||
pub fn body(&mut self) -> MessageBody<S> {
|
|
||||||
MessageBody::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads and parse `application/json` encoded body.
|
|
||||||
/// Return `JsonBody<T>` future. It resolves to a `T` value.
|
|
||||||
///
|
|
||||||
/// Returns error:
|
|
||||||
///
|
|
||||||
/// * content type is not `application/json`
|
|
||||||
/// * content length is greater than 256k
|
|
||||||
pub fn json<T: DeserializeOwned>(&mut self) -> JsonBody<S, T> {
|
|
||||||
JsonBody::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> Stream for ClientResponse<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
|
||||||
{
|
|
||||||
type Item = Result<Bytes, PayloadError>;
|
|
||||||
|
|
||||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
this.timeout.poll_timeout(cx)?;
|
|
||||||
|
|
||||||
Pin::new(&mut this.payload).poll_next(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> fmt::Debug for ClientResponse<S> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?;
|
|
||||||
writeln!(f, " headers:")?;
|
|
||||||
for (key, val) in self.headers().iter() {
|
|
||||||
writeln!(f, " {:?}: {:?}", key, val)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
/// Future that resolves to a complete HTTP message body.
|
|
||||||
pub struct MessageBody<S> {
|
|
||||||
length: Option<usize>,
|
|
||||||
timeout: ResponseTimeout,
|
|
||||||
body: Result<ReadBody<S>, Option<PayloadError>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> MessageBody<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>>,
|
|
||||||
{
|
|
||||||
/// Create `MessageBody` for request.
|
|
||||||
pub fn new(res: &mut ClientResponse<S>) -> MessageBody<S> {
|
|
||||||
let length = match res.headers().get(&header::CONTENT_LENGTH) {
|
|
||||||
Some(value) => {
|
|
||||||
let len = value.to_str().ok().and_then(|s| s.parse::<usize>().ok());
|
|
||||||
|
|
||||||
match len {
|
|
||||||
None => return Self::err(PayloadError::UnknownLength),
|
|
||||||
len => len,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
MessageBody {
|
|
||||||
length,
|
|
||||||
timeout: std::mem::take(&mut res.timeout),
|
|
||||||
body: Ok(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change max size of payload. By default max size is 2048kB
|
|
||||||
pub fn limit(mut self, limit: usize) -> Self {
|
|
||||||
if let Ok(ref mut body) = self.body {
|
|
||||||
body.limit = limit;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn err(e: PayloadError) -> Self {
|
|
||||||
MessageBody {
|
|
||||||
length: None,
|
|
||||||
timeout: ResponseTimeout::default(),
|
|
||||||
body: Err(Some(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> Future for MessageBody<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
|
||||||
{
|
|
||||||
type Output = Result<Bytes, PayloadError>;
|
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
|
|
||||||
match this.body {
|
|
||||||
Err(ref mut err) => Poll::Ready(Err(err.take().unwrap())),
|
|
||||||
Ok(ref mut body) => {
|
|
||||||
if let Some(len) = this.length.take() {
|
|
||||||
if len > body.limit {
|
|
||||||
return Poll::Ready(Err(PayloadError::Overflow));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timeout.poll_timeout(cx)?;
|
|
||||||
|
|
||||||
Pin::new(body).poll(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Response's payload json parser, it resolves to a deserialized `T` value.
|
|
||||||
///
|
|
||||||
/// Returns error:
|
|
||||||
///
|
|
||||||
/// * content type is not `application/json`
|
|
||||||
/// * content length is greater than 64k
|
|
||||||
pub struct JsonBody<S, U> {
|
|
||||||
length: Option<usize>,
|
|
||||||
err: Option<JsonPayloadError>,
|
|
||||||
timeout: ResponseTimeout,
|
|
||||||
fut: Option<ReadBody<S>>,
|
|
||||||
_phantom: PhantomData<U>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, U> JsonBody<S, U>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>>,
|
|
||||||
U: DeserializeOwned,
|
|
||||||
{
|
|
||||||
/// Create `JsonBody` for request.
|
|
||||||
pub fn new(res: &mut ClientResponse<S>) -> Self {
|
|
||||||
// check content-type
|
|
||||||
let json = if let Ok(Some(mime)) = res.mime_type() {
|
|
||||||
mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
if !json {
|
|
||||||
return JsonBody {
|
|
||||||
length: None,
|
|
||||||
fut: None,
|
|
||||||
timeout: ResponseTimeout::default(),
|
|
||||||
err: Some(JsonPayloadError::ContentType),
|
|
||||||
_phantom: PhantomData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut len = None;
|
|
||||||
|
|
||||||
if let Some(l) = res.headers().get(&header::CONTENT_LENGTH) {
|
|
||||||
if let Ok(s) = l.to_str() {
|
|
||||||
if let Ok(l) = s.parse::<usize>() {
|
|
||||||
len = Some(l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonBody {
|
|
||||||
length: len,
|
|
||||||
err: None,
|
|
||||||
timeout: std::mem::take(&mut res.timeout),
|
|
||||||
fut: Some(ReadBody::new(res.take_payload(), 65536)),
|
|
||||||
_phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change max size of payload. By default max size is 64kB
|
|
||||||
pub fn limit(mut self, limit: usize) -> Self {
|
|
||||||
if let Some(ref mut fut) = self.fut {
|
|
||||||
fut.limit = limit;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, U> Unpin for JsonBody<T, U>
|
|
||||||
where
|
|
||||||
T: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
|
||||||
U: DeserializeOwned,
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, U> Future for JsonBody<T, U>
|
|
||||||
where
|
|
||||||
T: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
|
||||||
U: DeserializeOwned,
|
|
||||||
{
|
|
||||||
type Output = Result<U, JsonPayloadError>;
|
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
||||||
if let Some(err) = self.err.take() {
|
|
||||||
return Poll::Ready(Err(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(len) = self.length.take() {
|
|
||||||
if len > self.fut.as_ref().unwrap().limit {
|
|
||||||
return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.timeout
|
|
||||||
.poll_timeout(cx)
|
|
||||||
.map_err(JsonPayloadError::Payload)?;
|
|
||||||
|
|
||||||
let body = ready!(Pin::new(&mut self.get_mut().fut.as_mut().unwrap()).poll(cx))?;
|
|
||||||
Poll::Ready(serde_json::from_slice::<U>(&body).map_err(JsonPayloadError::from))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ReadBody<S> {
|
|
||||||
stream: Payload<S>,
|
|
||||||
buf: BytesMut,
|
|
||||||
limit: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> ReadBody<S> {
|
|
||||||
fn new(stream: Payload<S>, limit: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
stream,
|
|
||||||
buf: BytesMut::new(),
|
|
||||||
limit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> Future for ReadBody<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
|
||||||
{
|
|
||||||
type Output = Result<Bytes, PayloadError>;
|
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
|
|
||||||
while let Some(chunk) = ready!(Pin::new(&mut this.stream).poll_next(cx)?) {
|
|
||||||
if (this.buf.len() + chunk.len()) > this.limit {
|
|
||||||
return Poll::Ready(Err(PayloadError::Overflow));
|
|
||||||
}
|
|
||||||
this.buf.extend_from_slice(&chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
Poll::Ready(Ok(this.buf.split().freeze()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{http::header, test::TestResponse};
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body() {
|
|
||||||
let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "xxxx")).finish();
|
|
||||||
match req.body().await.err().unwrap() {
|
|
||||||
PayloadError::UnknownLength => {}
|
|
||||||
_ => unreachable!("error"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "10000000")).finish();
|
|
||||||
match req.body().await.err().unwrap() {
|
|
||||||
PayloadError::Overflow => {}
|
|
||||||
_ => unreachable!("error"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut req = TestResponse::default()
|
|
||||||
.set_payload(Bytes::from_static(b"test"))
|
|
||||||
.finish();
|
|
||||||
assert_eq!(req.body().await.ok().unwrap(), Bytes::from_static(b"test"));
|
|
||||||
|
|
||||||
let mut req = TestResponse::default()
|
|
||||||
.set_payload(Bytes::from_static(b"11111111111111"))
|
|
||||||
.finish();
|
|
||||||
match req.body().limit(5).await.err().unwrap() {
|
|
||||||
PayloadError::Overflow => {}
|
|
||||||
_ => unreachable!("error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
||||||
struct MyObject {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool {
|
|
||||||
match err {
|
|
||||||
JsonPayloadError::Payload(PayloadError::Overflow) => {
|
|
||||||
matches!(other, JsonPayloadError::Payload(PayloadError::Overflow))
|
|
||||||
}
|
|
||||||
JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_json_body() {
|
|
||||||
let mut req = TestResponse::default().finish();
|
|
||||||
let json = JsonBody::<_, MyObject>::new(&mut req).await;
|
|
||||||
assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType));
|
|
||||||
|
|
||||||
let mut req = TestResponse::default()
|
|
||||||
.insert_header((
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
header::HeaderValue::from_static("application/text"),
|
|
||||||
))
|
|
||||||
.finish();
|
|
||||||
let json = JsonBody::<_, MyObject>::new(&mut req).await;
|
|
||||||
assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType));
|
|
||||||
|
|
||||||
let mut req = TestResponse::default()
|
|
||||||
.insert_header((
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
header::HeaderValue::from_static("application/json"),
|
|
||||||
))
|
|
||||||
.insert_header((
|
|
||||||
header::CONTENT_LENGTH,
|
|
||||||
header::HeaderValue::from_static("10000"),
|
|
||||||
))
|
|
||||||
.finish();
|
|
||||||
|
|
||||||
let json = JsonBody::<_, MyObject>::new(&mut req).limit(100).await;
|
|
||||||
assert!(json_eq(
|
|
||||||
json.err().unwrap(),
|
|
||||||
JsonPayloadError::Payload(PayloadError::Overflow)
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut req = TestResponse::default()
|
|
||||||
.insert_header((
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
header::HeaderValue::from_static("application/json"),
|
|
||||||
))
|
|
||||||
.insert_header((
|
|
||||||
header::CONTENT_LENGTH,
|
|
||||||
header::HeaderValue::from_static("16"),
|
|
||||||
))
|
|
||||||
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
|
|
||||||
.finish();
|
|
||||||
|
|
||||||
let json = JsonBody::<_, MyObject>::new(&mut req).await;
|
|
||||||
assert_eq!(
|
|
||||||
json.ok().unwrap(),
|
|
||||||
MyObject {
|
|
||||||
name: "test".to_owned()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
marker::PhantomData,
|
||||||
|
mem,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_http::{error::PayloadError, header, HttpMessage};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::{ready, Stream};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
use super::{read_body::ReadBody, ResponseTimeout, DEFAULT_BODY_LIMIT};
|
||||||
|
use crate::{error::JsonPayloadError, ClientResponse};
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
/// A `Future` that reads a body stream, parses JSON, resolving to a deserialized `T`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// `Future` implementation returns error if:
|
||||||
|
/// - content type is not `application/json`;
|
||||||
|
/// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB).
|
||||||
|
pub struct JsonBody<S, T> {
|
||||||
|
#[pin]
|
||||||
|
body: Option<ReadBody<S>>,
|
||||||
|
length: Option<usize>,
|
||||||
|
timeout: ResponseTimeout,
|
||||||
|
err: Option<JsonPayloadError>,
|
||||||
|
_phantom: PhantomData<T>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, T> JsonBody<S, T>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
/// Creates a JSON body stream reader from a response by taking its payload.
|
||||||
|
pub fn new(res: &mut ClientResponse<S>) -> Self {
|
||||||
|
// check content-type
|
||||||
|
let json = if let Ok(Some(mime)) = res.mime_type() {
|
||||||
|
mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if !json {
|
||||||
|
return JsonBody {
|
||||||
|
length: None,
|
||||||
|
body: None,
|
||||||
|
timeout: ResponseTimeout::default(),
|
||||||
|
err: Some(JsonPayloadError::ContentType),
|
||||||
|
_phantom: PhantomData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = res
|
||||||
|
.headers()
|
||||||
|
.get(&header::CONTENT_LENGTH)
|
||||||
|
.and_then(|len_hdr| len_hdr.to_str().ok())
|
||||||
|
.and_then(|len_str| len_str.parse::<usize>().ok());
|
||||||
|
|
||||||
|
JsonBody {
|
||||||
|
body: Some(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)),
|
||||||
|
length,
|
||||||
|
timeout: mem::take(&mut res.timeout),
|
||||||
|
err: None,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change max size of payload. Default limit is 2 MiB.
|
||||||
|
pub fn limit(mut self, limit: usize) -> Self {
|
||||||
|
if let Some(ref mut fut) = self.body {
|
||||||
|
fut.limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, T> Future for JsonBody<S, T>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
type Output = Result<T, JsonPayloadError>;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
let this = self.project();
|
||||||
|
|
||||||
|
if let Some(err) = this.err.take() {
|
||||||
|
return Poll::Ready(Err(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(len) = this.length.take() {
|
||||||
|
let body = Option::as_ref(&this.body).unwrap();
|
||||||
|
if len > body.limit {
|
||||||
|
return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout
|
||||||
|
.poll_timeout(cx)
|
||||||
|
.map_err(JsonPayloadError::Payload)?;
|
||||||
|
|
||||||
|
let body = ready!(this.body.as_pin_mut().unwrap().poll(cx))?;
|
||||||
|
Poll::Ready(serde_json::from_slice::<T>(&body).map_err(JsonPayloadError::from))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use actix_http::BoxedPayloadStream;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use static_assertions::assert_impl_all;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{http::header, test::TestResponse};
|
||||||
|
|
||||||
|
assert_impl_all!(JsonBody<BoxedPayloadStream, String>: Unpin);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
|
struct MyObject {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool {
|
||||||
|
match err {
|
||||||
|
JsonPayloadError::Payload(PayloadError::Overflow) => {
|
||||||
|
matches!(other, JsonPayloadError::Payload(PayloadError::Overflow))
|
||||||
|
}
|
||||||
|
JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn read_json_body() {
|
||||||
|
let mut req = TestResponse::default().finish();
|
||||||
|
let json = JsonBody::<_, MyObject>::new(&mut req).await;
|
||||||
|
assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType));
|
||||||
|
|
||||||
|
let mut req = TestResponse::default()
|
||||||
|
.insert_header((
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
header::HeaderValue::from_static("application/text"),
|
||||||
|
))
|
||||||
|
.finish();
|
||||||
|
let json = JsonBody::<_, MyObject>::new(&mut req).await;
|
||||||
|
assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType));
|
||||||
|
|
||||||
|
let mut req = TestResponse::default()
|
||||||
|
.insert_header((
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
header::HeaderValue::from_static("application/json"),
|
||||||
|
))
|
||||||
|
.insert_header((
|
||||||
|
header::CONTENT_LENGTH,
|
||||||
|
header::HeaderValue::from_static("10000"),
|
||||||
|
))
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
let json = JsonBody::<_, MyObject>::new(&mut req).limit(100).await;
|
||||||
|
assert!(json_eq(
|
||||||
|
json.err().unwrap(),
|
||||||
|
JsonPayloadError::Payload(PayloadError::Overflow)
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut req = TestResponse::default()
|
||||||
|
.insert_header((
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
header::HeaderValue::from_static("application/json"),
|
||||||
|
))
|
||||||
|
.insert_header((
|
||||||
|
header::CONTENT_LENGTH,
|
||||||
|
header::HeaderValue::from_static("16"),
|
||||||
|
))
|
||||||
|
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
let json = JsonBody::<_, MyObject>::new(&mut req).await;
|
||||||
|
assert_eq!(
|
||||||
|
json.ok().unwrap(),
|
||||||
|
MyObject {
|
||||||
|
name: "test".to_owned()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
use std::{future::Future, io, pin::Pin, task::Context};
|
||||||
|
|
||||||
|
use actix_http::error::PayloadError;
|
||||||
|
use actix_rt::time::Sleep;
|
||||||
|
|
||||||
|
mod json_body;
|
||||||
|
mod read_body;
|
||||||
|
mod response;
|
||||||
|
mod response_body;
|
||||||
|
|
||||||
|
pub use self::json_body::JsonBody;
|
||||||
|
pub use self::response::ClientResponse;
|
||||||
|
#[allow(deprecated)]
|
||||||
|
pub use self::response_body::{MessageBody, ResponseBody};
|
||||||
|
|
||||||
|
/// Default body size limit: 2 MiB
|
||||||
|
const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Helper enum with reusable sleep passed from `SendClientResponse`.
|
||||||
|
///
|
||||||
|
/// See [`ClientResponse::_timeout`] for reason.
|
||||||
|
pub(crate) enum ResponseTimeout {
|
||||||
|
Disabled(Option<Pin<Box<Sleep>>>),
|
||||||
|
Enabled(Pin<Box<Sleep>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ResponseTimeout {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Disabled(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseTimeout {
|
||||||
|
fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> {
|
||||||
|
match *self {
|
||||||
|
Self::Enabled(ref mut timeout) => {
|
||||||
|
if timeout.as_mut().poll(cx).is_ready() {
|
||||||
|
Err(PayloadError::Io(io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
"Response Payload IO timed out",
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Disabled(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_http::{error::PayloadError, Payload};
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use futures_core::{ready, Stream};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
pub(crate) struct ReadBody<S> {
|
||||||
|
#[pin]
|
||||||
|
pub(crate) stream: Payload<S>,
|
||||||
|
pub(crate) buf: BytesMut,
|
||||||
|
pub(crate) limit: usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ReadBody<S> {
|
||||||
|
pub(crate) fn new(stream: Payload<S>, limit: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
stream,
|
||||||
|
buf: BytesMut::new(),
|
||||||
|
limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Future for ReadBody<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
|
{
|
||||||
|
type Output = Result<Bytes, PayloadError>;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
let mut this = self.project();
|
||||||
|
|
||||||
|
while let Some(chunk) = ready!(this.stream.as_mut().poll_next(cx)?) {
|
||||||
|
if (this.buf.len() + chunk.len()) > *this.limit {
|
||||||
|
return Poll::Ready(Err(PayloadError::Overflow));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buf.extend_from_slice(&chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Ready(Ok(this.buf.split().freeze()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use static_assertions::assert_impl_all;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::any_body::AnyBody;
|
||||||
|
|
||||||
|
assert_impl_all!(ReadBody<()>: Unpin);
|
||||||
|
assert_impl_all!(ReadBody<AnyBody>: Unpin);
|
||||||
|
}
|
|
@ -0,0 +1,258 @@
|
||||||
|
use std::{
|
||||||
|
cell::{Ref, RefMut},
|
||||||
|
fmt, mem,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_http::{
|
||||||
|
error::PayloadError, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage,
|
||||||
|
Payload, ResponseHead, StatusCode, Version,
|
||||||
|
};
|
||||||
|
use actix_rt::time::{sleep, Sleep};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::Stream;
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
use crate::cookie::{Cookie, ParseError as CookieParseError};
|
||||||
|
|
||||||
|
use super::{JsonBody, ResponseBody, ResponseTimeout};
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
/// Client Response
|
||||||
|
pub struct ClientResponse<S = BoxedPayloadStream> {
|
||||||
|
pub(crate) head: ResponseHead,
|
||||||
|
#[pin]
|
||||||
|
pub(crate) payload: Payload<S>,
|
||||||
|
pub(crate) timeout: ResponseTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ClientResponse<S> {
|
||||||
|
/// Create new Request instance
|
||||||
|
pub(crate) fn new(head: ResponseHead, payload: Payload<S>) -> Self {
|
||||||
|
ClientResponse {
|
||||||
|
head,
|
||||||
|
payload,
|
||||||
|
timeout: ResponseTimeout::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn head(&self) -> &ResponseHead {
|
||||||
|
&self.head
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the Request Version.
|
||||||
|
#[inline]
|
||||||
|
pub fn version(&self) -> Version {
|
||||||
|
self.head().version
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the status from the server.
|
||||||
|
#[inline]
|
||||||
|
pub fn status(&self) -> StatusCode {
|
||||||
|
self.head().status
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Returns request's headers.
|
||||||
|
pub fn headers(&self) -> &HeaderMap {
|
||||||
|
&self.head().headers
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a body and return previous body value
|
||||||
|
pub fn map_body<F, U>(mut self, f: F) -> ClientResponse<U>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut ResponseHead, Payload<S>) -> Payload<U>,
|
||||||
|
{
|
||||||
|
let payload = f(&mut self.head, self.payload);
|
||||||
|
|
||||||
|
ClientResponse {
|
||||||
|
payload,
|
||||||
|
head: self.head,
|
||||||
|
timeout: self.timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a timeout duration for [`ClientResponse`](self::ClientResponse).
|
||||||
|
///
|
||||||
|
/// This duration covers the duration of processing the response body stream
|
||||||
|
/// and would end it as timeout error when deadline met.
|
||||||
|
///
|
||||||
|
/// Disabled by default.
|
||||||
|
pub fn timeout(self, dur: Duration) -> Self {
|
||||||
|
let timeout = match self.timeout {
|
||||||
|
ResponseTimeout::Disabled(Some(mut timeout))
|
||||||
|
| ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) {
|
||||||
|
Some(deadline) => {
|
||||||
|
timeout.as_mut().reset(deadline.into());
|
||||||
|
ResponseTimeout::Enabled(timeout)
|
||||||
|
}
|
||||||
|
None => ResponseTimeout::Enabled(Box::pin(sleep(dur))),
|
||||||
|
},
|
||||||
|
_ => ResponseTimeout::Enabled(Box::pin(sleep(dur))),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
payload: self.payload,
|
||||||
|
head: self.head,
|
||||||
|
timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This method does not enable timeout. It's used to pass the boxed `Sleep` from
|
||||||
|
/// `SendClientRequest` and reuse it's heap allocation together with it's slot in
|
||||||
|
/// timer wheel.
|
||||||
|
pub(crate) fn _timeout(mut self, timeout: Option<Pin<Box<Sleep>>>) -> Self {
|
||||||
|
self.timeout = ResponseTimeout::Disabled(timeout);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load request cookies.
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
||||||
|
struct Cookies(Vec<Cookie<'static>>);
|
||||||
|
|
||||||
|
if self.extensions().get::<Cookies>().is_none() {
|
||||||
|
let mut cookies = Vec::new();
|
||||||
|
for hdr in self.headers().get_all(&actix_http::header::SET_COOKIE) {
|
||||||
|
let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
|
||||||
|
cookies.push(Cookie::parse_encoded(s)?.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")]
|
||||||
|
pub 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<S> ClientResponse<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
|
{
|
||||||
|
/// Returns a [`Future`] that consumes the body stream and resolves to [`Bytes`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// `Future` implementation returns error if:
|
||||||
|
/// - content type is not `application/json`
|
||||||
|
/// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB)
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// # use awc::Client;
|
||||||
|
/// # use bytes::Bytes;
|
||||||
|
/// # #[actix_rt::main]
|
||||||
|
/// # async fn async_ctx() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let client = Client::default();
|
||||||
|
/// let mut res = client.get("https://httpbin.org/robots.txt").send().await?;
|
||||||
|
/// let body: Bytes = res.body().await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`Future`]: std::future::Future
|
||||||
|
pub fn body(&mut self) -> ResponseBody<S> {
|
||||||
|
ResponseBody::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [`Future`] consumes the body stream, parses JSON, and resolves to a deserialized
|
||||||
|
/// `T` value.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Future returns error if:
|
||||||
|
/// - content type is not `application/json`;
|
||||||
|
/// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// # use awc::Client;
|
||||||
|
/// # #[actix_rt::main]
|
||||||
|
/// # async fn async_ctx() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let client = Client::default();
|
||||||
|
/// let mut res = client.get("https://httpbin.org/json").send().await?;
|
||||||
|
/// let val = res.json::<serde_json::Value>().await?;
|
||||||
|
/// assert!(val.is_object());
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`Future`]: std::future::Future
|
||||||
|
pub fn json<T: DeserializeOwned>(&mut self) -> JsonBody<S, T> {
|
||||||
|
JsonBody::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> fmt::Debug for ClientResponse<S> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?;
|
||||||
|
writeln!(f, " headers:")?;
|
||||||
|
for (key, val) in self.headers().iter() {
|
||||||
|
writeln!(f, " {:?}: {:?}", key, val)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> HttpMessage for ClientResponse<S> {
|
||||||
|
type Stream = S;
|
||||||
|
|
||||||
|
fn headers(&self) -> &HeaderMap {
|
||||||
|
&self.head.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_payload(&mut self) -> Payload<S> {
|
||||||
|
mem::replace(&mut self.payload, Payload::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extensions(&self) -> Ref<'_, Extensions> {
|
||||||
|
self.head.extensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
|
||||||
|
self.head.extensions_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Stream for ClientResponse<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
||||||
|
{
|
||||||
|
type Item = Result<Bytes, PayloadError>;
|
||||||
|
|
||||||
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
let this = self.project();
|
||||||
|
this.timeout.poll_timeout(cx)?;
|
||||||
|
this.payload.poll_next(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use static_assertions::assert_impl_all;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::any_body::AnyBody;
|
||||||
|
|
||||||
|
assert_impl_all!(ClientResponse: Unpin);
|
||||||
|
assert_impl_all!(ClientResponse<()>: Unpin);
|
||||||
|
assert_impl_all!(ClientResponse<AnyBody>: Unpin);
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
mem,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_http::{error::PayloadError, header, HttpMessage};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::Stream;
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
|
use super::{read_body::ReadBody, ResponseTimeout, DEFAULT_BODY_LIMIT};
|
||||||
|
use crate::ClientResponse;
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
/// A `Future` that reads a body stream, resolving as [`Bytes`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// `Future` implementation returns error if:
|
||||||
|
/// - content type is not `application/json`;
|
||||||
|
/// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB).
|
||||||
|
pub struct ResponseBody<S> {
|
||||||
|
#[pin]
|
||||||
|
body: Option<ReadBody<S>>,
|
||||||
|
length: Option<usize>,
|
||||||
|
timeout: ResponseTimeout,
|
||||||
|
err: Option<PayloadError>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deprecated(since = "3.0.0", note = "Renamed to `ResponseBody`.")]
|
||||||
|
pub type MessageBody<B> = ResponseBody<B>;
|
||||||
|
|
||||||
|
impl<S> ResponseBody<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
|
{
|
||||||
|
/// Creates a body stream reader from a response by taking its payload.
|
||||||
|
pub fn new(res: &mut ClientResponse<S>) -> ResponseBody<S> {
|
||||||
|
let length = match res.headers().get(&header::CONTENT_LENGTH) {
|
||||||
|
Some(value) => {
|
||||||
|
let len = value.to_str().ok().and_then(|s| s.parse::<usize>().ok());
|
||||||
|
|
||||||
|
match len {
|
||||||
|
None => return Self::err(PayloadError::UnknownLength),
|
||||||
|
len => len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
ResponseBody {
|
||||||
|
body: Some(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)),
|
||||||
|
length,
|
||||||
|
timeout: mem::take(&mut res.timeout),
|
||||||
|
err: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change max size limit of payload.
|
||||||
|
///
|
||||||
|
/// The default limit is 2 MiB.
|
||||||
|
pub fn limit(mut self, limit: usize) -> Self {
|
||||||
|
if let Some(ref mut body) = self.body {
|
||||||
|
body.limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn err(err: PayloadError) -> Self {
|
||||||
|
ResponseBody {
|
||||||
|
body: None,
|
||||||
|
length: None,
|
||||||
|
timeout: ResponseTimeout::default(),
|
||||||
|
err: Some(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Future for ResponseBody<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
|
{
|
||||||
|
type Output = Result<Bytes, PayloadError>;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
let this = self.project();
|
||||||
|
|
||||||
|
if let Some(err) = this.err.take() {
|
||||||
|
return Poll::Ready(Err(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(len) = this.length.take() {
|
||||||
|
let body = Option::as_ref(&this.body).unwrap();
|
||||||
|
if len > body.limit {
|
||||||
|
return Poll::Ready(Err(PayloadError::Overflow));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout.poll_timeout(cx)?;
|
||||||
|
|
||||||
|
this.body.as_pin_mut().unwrap().poll(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use static_assertions::assert_impl_all;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{http::header, test::TestResponse};
|
||||||
|
|
||||||
|
assert_impl_all!(ResponseBody<()>: Unpin);
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn read_body() {
|
||||||
|
let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "xxxx")).finish();
|
||||||
|
match req.body().await.err().unwrap() {
|
||||||
|
PayloadError::UnknownLength => {}
|
||||||
|
_ => unreachable!("error"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "10000000")).finish();
|
||||||
|
match req.body().await.err().unwrap() {
|
||||||
|
PayloadError::Overflow => {}
|
||||||
|
_ => unreachable!("error"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut req = TestResponse::default()
|
||||||
|
.set_payload(Bytes::from_static(b"test"))
|
||||||
|
.finish();
|
||||||
|
assert_eq!(req.body().await.ok().unwrap(), Bytes::from_static(b"test"));
|
||||||
|
|
||||||
|
let mut req = TestResponse::default()
|
||||||
|
.set_payload(Bytes::from_static(b"11111111111111"))
|
||||||
|
.finish();
|
||||||
|
match req.body().limit(5).await.err().unwrap() {
|
||||||
|
PayloadError::Overflow => {}
|
||||||
|
_ => unreachable!("error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
body::BodyStream,
|
body::{BodyStream, MessageBody},
|
||||||
error::HttpError,
|
error::HttpError,
|
||||||
header::{self, HeaderMap, HeaderName, TryIntoHeaderValue},
|
header::{self, HeaderMap, HeaderName, TryIntoHeaderValue},
|
||||||
RequestHead, RequestHeadType,
|
RequestHead, RequestHeadType,
|
||||||
|
@ -20,12 +20,13 @@ use futures_core::Stream;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[cfg(feature = "__compress")]
|
#[cfg(feature = "__compress")]
|
||||||
use actix_http::{encoding::Decoder, header::ContentEncoding, Payload, PayloadStream};
|
use actix_http::{encoding::Decoder, header::ContentEncoding, Payload};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any_body::AnyBody,
|
any_body::AnyBody,
|
||||||
|
client::ClientConfig,
|
||||||
error::{FreezeRequestError, InvalidUrl, SendRequestError},
|
error::{FreezeRequestError, InvalidUrl, SendRequestError},
|
||||||
BoxError, ClientConfig, ClientResponse, ConnectRequest, ConnectResponse,
|
BoxError, ClientResponse, ConnectRequest, ConnectResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, From)]
|
#[derive(Debug, From)]
|
||||||
|
@ -91,7 +92,7 @@ impl SendClientRequest {
|
||||||
|
|
||||||
#[cfg(feature = "__compress")]
|
#[cfg(feature = "__compress")]
|
||||||
impl Future for SendClientRequest {
|
impl Future for SendClientRequest {
|
||||||
type Output = Result<ClientResponse<Decoder<Payload<PayloadStream>>>, SendRequestError>;
|
type Output = Result<ClientResponse<Decoder<Payload>>, SendRequestError>;
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
|
@ -108,12 +109,13 @@ impl Future for SendClientRequest {
|
||||||
res.into_client_response()._timeout(delay.take()).map_body(
|
res.into_client_response()._timeout(delay.take()).map_body(
|
||||||
|head, payload| {
|
|head, payload| {
|
||||||
if *response_decompress {
|
if *response_decompress {
|
||||||
Payload::Stream(Decoder::from_headers(payload, &head.headers))
|
Payload::Stream {
|
||||||
|
payload: Decoder::from_headers(payload, &head.headers),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Payload::Stream(Decoder::new(
|
Payload::Stream {
|
||||||
payload,
|
payload: Decoder::new(payload, ContentEncoding::Identity),
|
||||||
ContentEncoding::Identity,
|
}
|
||||||
))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -179,24 +181,23 @@ pub(crate) enum RequestSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestSender {
|
impl RequestSender {
|
||||||
pub(crate) fn send_body<B>(
|
pub(crate) fn send_body(
|
||||||
self,
|
self,
|
||||||
addr: Option<net::SocketAddr>,
|
addr: Option<net::SocketAddr>,
|
||||||
response_decompress: bool,
|
response_decompress: bool,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
config: &ClientConfig,
|
config: &ClientConfig,
|
||||||
body: B,
|
body: impl MessageBody + 'static,
|
||||||
) -> SendClientRequest
|
) -> SendClientRequest {
|
||||||
where
|
|
||||||
B: Into<AnyBody>,
|
|
||||||
{
|
|
||||||
let req = match self {
|
let req = match self {
|
||||||
RequestSender::Owned(head) => {
|
RequestSender::Owned(head) => ConnectRequest::Client(
|
||||||
ConnectRequest::Client(RequestHeadType::Owned(head), body.into(), addr)
|
RequestHeadType::Owned(head),
|
||||||
}
|
AnyBody::from_message_body(body).into_boxed(),
|
||||||
|
addr,
|
||||||
|
),
|
||||||
RequestSender::Rc(head, extra_headers) => ConnectRequest::Client(
|
RequestSender::Rc(head, extra_headers) => ConnectRequest::Client(
|
||||||
RequestHeadType::Rc(head, extra_headers),
|
RequestHeadType::Rc(head, extra_headers),
|
||||||
body.into(),
|
AnyBody::from_message_body(body).into_boxed(),
|
||||||
addr,
|
addr,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -206,15 +207,15 @@ impl RequestSender {
|
||||||
SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout))
|
SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn send_json<T: Serialize>(
|
pub(crate) fn send_json(
|
||||||
mut self,
|
mut self,
|
||||||
addr: Option<net::SocketAddr>,
|
addr: Option<net::SocketAddr>,
|
||||||
response_decompress: bool,
|
response_decompress: bool,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
config: &ClientConfig,
|
config: &ClientConfig,
|
||||||
value: &T,
|
value: impl Serialize,
|
||||||
) -> SendClientRequest {
|
) -> SendClientRequest {
|
||||||
let body = match serde_json::to_string(value) {
|
let body = match serde_json::to_string(&value) {
|
||||||
Ok(body) => body,
|
Ok(body) => body,
|
||||||
Err(err) => return PrepForSendingError::Json(err).into(),
|
Err(err) => return PrepForSendingError::Json(err).into(),
|
||||||
};
|
};
|
||||||
|
@ -223,24 +224,16 @@ impl RequestSender {
|
||||||
return e.into();
|
return e.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_body(
|
self.send_body(addr, response_decompress, timeout, config, body)
|
||||||
addr,
|
|
||||||
response_decompress,
|
|
||||||
timeout,
|
|
||||||
config,
|
|
||||||
AnyBody::Bytes {
|
|
||||||
body: Bytes::from(body),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn send_form<T: Serialize>(
|
pub(crate) fn send_form(
|
||||||
mut self,
|
mut self,
|
||||||
addr: Option<net::SocketAddr>,
|
addr: Option<net::SocketAddr>,
|
||||||
response_decompress: bool,
|
response_decompress: bool,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
config: &ClientConfig,
|
config: &ClientConfig,
|
||||||
value: &T,
|
value: impl Serialize,
|
||||||
) -> SendClientRequest {
|
) -> SendClientRequest {
|
||||||
let body = match serde_urlencoded::to_string(value) {
|
let body = match serde_urlencoded::to_string(value) {
|
||||||
Ok(body) => body,
|
Ok(body) => body,
|
||||||
|
@ -248,21 +241,13 @@ impl RequestSender {
|
||||||
};
|
};
|
||||||
|
|
||||||
// set content-type
|
// set content-type
|
||||||
if let Err(e) =
|
if let Err(err) =
|
||||||
self.set_header_if_none(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
self.set_header_if_none(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||||
{
|
{
|
||||||
return e.into();
|
return err.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_body(
|
self.send_body(addr, response_decompress, timeout, config, body)
|
||||||
addr,
|
|
||||||
response_decompress,
|
|
||||||
timeout,
|
|
||||||
config,
|
|
||||||
AnyBody::Bytes {
|
|
||||||
body: Bytes::from(body),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn send_stream<S, E>(
|
pub(crate) fn send_stream<S, E>(
|
||||||
|
@ -274,7 +259,7 @@ impl RequestSender {
|
||||||
stream: S,
|
stream: S,
|
||||||
) -> SendClientRequest
|
) -> SendClientRequest
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||||
E: Into<BoxError> + 'static,
|
E: Into<BoxError> + 'static,
|
||||||
{
|
{
|
||||||
self.send_body(
|
self.send_body(
|
||||||
|
@ -282,7 +267,7 @@ impl RequestSender {
|
||||||
response_decompress,
|
response_decompress,
|
||||||
timeout,
|
timeout,
|
||||||
config,
|
config,
|
||||||
AnyBody::new_boxed(BodyStream::new(stream)),
|
BodyStream::new(stream),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +278,7 @@ impl RequestSender {
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
config: &ClientConfig,
|
config: &ClientConfig,
|
||||||
) -> SendClientRequest {
|
) -> SendClientRequest {
|
||||||
self.send_body(addr, response_decompress, timeout, config, AnyBody::empty())
|
self.send_body(addr, response_decompress, timeout, config, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError>
|
fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError>
|
||||||
|
|
|
@ -65,7 +65,7 @@ impl TestResponse {
|
||||||
|
|
||||||
/// Set response's payload
|
/// Set response's payload
|
||||||
pub fn set_payload<B: Into<Bytes>>(mut self, data: B) -> Self {
|
pub fn set_payload<B: Into<Bytes>>(mut self, data: B) -> Self {
|
||||||
let mut payload = h1::Payload::empty();
|
let (_, mut payload) = h1::Payload::create(true);
|
||||||
payload.unread_data(data.into());
|
payload.unread_data(data.into());
|
||||||
self.payload = Some(payload.into());
|
self.payload = Some(payload.into());
|
||||||
self
|
self
|
||||||
|
@ -90,7 +90,8 @@ impl TestResponse {
|
||||||
if let Some(pl) = self.payload {
|
if let Some(pl) = self.payload {
|
||||||
ClientResponse::new(head, pl)
|
ClientResponse::new(head, pl)
|
||||||
} else {
|
} else {
|
||||||
ClientResponse::new(head, h1::Payload::empty().into())
|
let (_, payload) = h1::Payload::create(true);
|
||||||
|
ClientResponse::new(head, payload.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,19 +31,19 @@ use std::{convert::TryFrom, fmt, net::SocketAddr, str};
|
||||||
use actix_codec::Framed;
|
use actix_codec::Framed;
|
||||||
use actix_http::{ws, Payload, RequestHead};
|
use actix_http::{ws, Payload, RequestHead};
|
||||||
use actix_rt::time::timeout;
|
use actix_rt::time::timeout;
|
||||||
use actix_service::Service;
|
use actix_service::Service as _;
|
||||||
|
|
||||||
pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message};
|
pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
client::ClientConfig,
|
||||||
connect::{BoxedSocket, ConnectRequest},
|
connect::{BoxedSocket, ConnectRequest},
|
||||||
error::{HttpError, InvalidUrl, SendRequestError, WsClientError},
|
error::{HttpError, InvalidUrl, SendRequestError, WsClientError},
|
||||||
http::{
|
http::{
|
||||||
header::{self, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION},
|
header::{self, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION},
|
||||||
ConnectionType, Method, StatusCode, Uri, Version,
|
ConnectionType, Method, StatusCode, Uri, Version,
|
||||||
},
|
},
|
||||||
response::ClientResponse,
|
ClientResponse,
|
||||||
ClientConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
|
@ -300,13 +300,16 @@ impl WebsocketsRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.head.set_connection_type(ConnectionType::Upgrade);
|
self.head.set_connection_type(ConnectionType::Upgrade);
|
||||||
|
|
||||||
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
|
const HV_WEBSOCKET: HeaderValue = HeaderValue::from_static("websocket");
|
||||||
|
self.head.headers.insert(header::UPGRADE, HV_WEBSOCKET);
|
||||||
|
|
||||||
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
|
const HV_THIRTEEN: HeaderValue = HeaderValue::from_static("13");
|
||||||
self.head
|
self.head
|
||||||
.headers
|
.headers
|
||||||
.insert(header::UPGRADE, HeaderValue::from_static("websocket"));
|
.insert(header::SEC_WEBSOCKET_VERSION, HV_THIRTEEN);
|
||||||
self.head.headers.insert(
|
|
||||||
header::SEC_WEBSOCKET_VERSION,
|
|
||||||
HeaderValue::from_static("13"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(protocols) = self.protocols.take() {
|
if let Some(protocols) = self.protocols.take() {
|
||||||
self.head.headers.insert(
|
self.head.headers.insert(
|
||||||
|
|
42
src/app.rs
42
src/app.rs
|
@ -122,9 +122,10 @@ impl<T> App<T> {
|
||||||
self.app_data(Data::new(data))
|
self.app_data(Data::new(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add application data factory. This function is similar to `.data()` but it accepts a
|
/// Add application data factory that resolves asynchronously.
|
||||||
/// "data factory". Data values are constructed asynchronously during application
|
///
|
||||||
/// initialization, before the server starts accepting requests.
|
/// Data items are constructed during application initialization, before the server starts
|
||||||
|
/// accepting requests.
|
||||||
pub fn data_factory<F, Out, D, E>(mut self, data: F) -> Self
|
pub fn data_factory<F, Out, D, E>(mut self, data: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn() -> Out + 'static,
|
F: Fn() -> Out + 'static,
|
||||||
|
@ -150,6 +151,7 @@ impl<T> App<T> {
|
||||||
}
|
}
|
||||||
.boxed_local()
|
.boxed_local()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,11 +202,9 @@ impl<T> App<T> {
|
||||||
/// "Welcome!"
|
/// "Welcome!"
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new()
|
||||||
/// let app = App::new()
|
/// .route("/test1", web::get().to(index))
|
||||||
/// .route("/test1", web::get().to(index))
|
/// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed()));
|
||||||
/// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed()));
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn route(self, path: &str, mut route: Route) -> Self {
|
pub fn route(self, path: &str, mut route: Route) -> Self {
|
||||||
self.service(
|
self.service(
|
||||||
|
@ -243,13 +243,11 @@ impl<T> App<T> {
|
||||||
/// "Welcome!"
|
/// "Welcome!"
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new()
|
||||||
/// let app = App::new()
|
/// .service(
|
||||||
/// .service(
|
/// web::resource("/index.html").route(web::get().to(index)))
|
||||||
/// web::resource("/index.html").route(web::get().to(index)))
|
/// .default_service(
|
||||||
/// .default_service(
|
/// web::route().to(|| HttpResponse::NotFound()));
|
||||||
/// web::route().to(|| HttpResponse::NotFound()));
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// It is also possible to use static files as default service.
|
/// It is also possible to use static files as default service.
|
||||||
|
@ -257,14 +255,12 @@ impl<T> App<T> {
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, App, HttpResponse};
|
/// use actix_web::{web, App, HttpResponse};
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new()
|
||||||
/// let app = App::new()
|
/// .service(
|
||||||
/// .service(
|
/// web::resource("/index.html").to(|| HttpResponse::Ok()))
|
||||||
/// web::resource("/index.html").to(|| HttpResponse::Ok()))
|
/// .default_service(
|
||||||
/// .default_service(
|
/// web::to(|| HttpResponse::NotFound())
|
||||||
/// web::to(|| HttpResponse::NotFound())
|
/// );
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn default_service<F, U>(mut self, svc: F) -> Self
|
pub fn default_service<F, U>(mut self, svc: F) -> Self
|
||||||
where
|
where
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
use std::{cell::RefCell, mem, rc::Rc};
|
use std::{cell::RefCell, mem, rc::Rc};
|
||||||
|
|
||||||
use actix_http::{Extensions, Request};
|
use actix_http::Request;
|
||||||
use actix_router::{Path, ResourceDef, Router, Url};
|
use actix_router::{Path, ResourceDef, Router, Url};
|
||||||
use actix_service::{boxed, fn_service, Service, ServiceFactory};
|
use actix_service::{boxed, fn_service, Service, ServiceFactory};
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
use futures_util::future::join_all;
|
use futures_util::future::join_all;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
body::BoxBody,
|
||||||
config::{AppConfig, AppService},
|
config::{AppConfig, AppService},
|
||||||
data::FnDataFactory,
|
data::FnDataFactory,
|
||||||
|
dev::Extensions,
|
||||||
guard::Guard,
|
guard::Guard,
|
||||||
request::{HttpRequest, HttpRequestPool},
|
request::{HttpRequest, HttpRequestPool},
|
||||||
rmap::ResourceMap,
|
rmap::ResourceMap,
|
||||||
|
@ -236,6 +238,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppRoutingFactory {
|
pub struct AppRoutingFactory {
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
services: Rc<
|
services: Rc<
|
||||||
[(
|
[(
|
||||||
ResourceDef,
|
ResourceDef,
|
||||||
|
@ -296,7 +299,7 @@ pub struct AppRouting {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Service<ServiceRequest> for AppRouting {
|
impl Service<ServiceRequest> for AppRouting {
|
||||||
type Response = ServiceResponse;
|
type Response = ServiceResponse<BoxBody>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
@ -305,12 +308,15 @@ impl Service<ServiceRequest> for AppRouting {
|
||||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||||
let res = self.router.recognize_fn(&mut req, |req, guards| {
|
let res = self.router.recognize_fn(&mut req, |req, guards| {
|
||||||
if let Some(ref guards) = guards {
|
if let Some(ref guards) = guards {
|
||||||
for f in guards {
|
let guard_ctx = req.guard_ctx();
|
||||||
if !f.check(req.head()) {
|
|
||||||
|
for guard in guards {
|
||||||
|
if !guard.check(&guard_ctx) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ pub struct AppService {
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
root: bool,
|
root: bool,
|
||||||
default: Rc<HttpNewService>,
|
default: Rc<HttpNewService>,
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
services: Vec<(
|
services: Vec<(
|
||||||
ResourceDef,
|
ResourceDef,
|
||||||
HttpNewService,
|
HttpNewService,
|
||||||
|
@ -48,6 +49,7 @@ impl AppService {
|
||||||
self.root
|
self.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
pub(crate) fn into_services(
|
pub(crate) fn into_services(
|
||||||
self,
|
self,
|
||||||
) -> (
|
) -> (
|
||||||
|
@ -126,7 +128,7 @@ impl AppConfig {
|
||||||
|
|
||||||
/// Server host name.
|
/// Server host name.
|
||||||
///
|
///
|
||||||
/// Host name is used by application router as a hostname for url generation.
|
/// Host name is used by application router as a hostname for URL generation.
|
||||||
/// Check [ConnectionInfo](super::dev::ConnectionInfo::host())
|
/// Check [ConnectionInfo](super::dev::ConnectionInfo::host())
|
||||||
/// documentation for more information.
|
/// documentation for more information.
|
||||||
///
|
///
|
||||||
|
@ -135,7 +137,7 @@ impl AppConfig {
|
||||||
&self.host
|
&self.host
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if connection is secure(https)
|
/// Returns true if connection is secure (i.e., running over `https:`).
|
||||||
pub fn secure(&self) -> bool {
|
pub fn secure(&self) -> bool {
|
||||||
self.secure
|
self.secure
|
||||||
}
|
}
|
||||||
|
|
23
src/dev.rs
23
src/dev.rs
|
@ -3,6 +3,16 @@
|
||||||
//! Most users will not have to interact with the types in this module, but it is useful for those
|
//! Most users will not have to interact with the types in this module, but it is useful for those
|
||||||
//! writing extractors, middleware, libraries, or interacting with the service API directly.
|
//! writing extractors, middleware, libraries, or interacting with the service API directly.
|
||||||
|
|
||||||
|
pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead};
|
||||||
|
pub use actix_router::{Path, ResourceDef, ResourcePath, Url};
|
||||||
|
pub use actix_server::{Server, ServerHandle};
|
||||||
|
pub use actix_service::{
|
||||||
|
always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "__compress")]
|
||||||
|
pub use actix_http::encoding::Decoder as Decompress;
|
||||||
|
|
||||||
pub use crate::config::{AppConfig, AppService};
|
pub use crate::config::{AppConfig, AppService};
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use crate::handler::Handler;
|
pub use crate::handler::Handler;
|
||||||
|
@ -12,16 +22,6 @@ pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, We
|
||||||
|
|
||||||
pub use crate::types::{JsonBody, Readlines, UrlEncoded};
|
pub use crate::types::{JsonBody, Readlines, UrlEncoded};
|
||||||
|
|
||||||
pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead};
|
|
||||||
pub use actix_router::{Path, ResourceDef, ResourcePath, Url};
|
|
||||||
pub use actix_server::{Server, ServerHandle};
|
|
||||||
pub use actix_service::{
|
|
||||||
always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "__compress")]
|
|
||||||
pub use actix_http::encoding::Decoder as Decompress;
|
|
||||||
|
|
||||||
use crate::http::header::ContentEncoding;
|
use crate::http::header::ContentEncoding;
|
||||||
|
|
||||||
use actix_router::Patterns;
|
use actix_router::Patterns;
|
||||||
|
@ -44,7 +44,6 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns {
|
||||||
|
|
||||||
patterns
|
patterns
|
||||||
}
|
}
|
||||||
struct Enc(ContentEncoding);
|
|
||||||
|
|
||||||
/// Helper trait that allows to set specific encoding for response.
|
/// Helper trait that allows to set specific encoding for response.
|
||||||
pub trait BodyEncoding {
|
pub trait BodyEncoding {
|
||||||
|
@ -68,6 +67,8 @@ impl BodyEncoding for actix_http::ResponseBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Enc(ContentEncoding);
|
||||||
|
|
||||||
impl<B> BodyEncoding for actix_http::Response<B> {
|
impl<B> BodyEncoding for actix_http::Response<B> {
|
||||||
fn get_encoding(&self) -> Option<ContentEncoding> {
|
fn get_encoding(&self) -> Option<ContentEncoding> {
|
||||||
self.extensions().get::<Enc>().map(|enc| enc.0)
|
self.extensions().get::<Enc>().map(|enc| enc.0)
|
||||||
|
|
772
src/guard.rs
772
src/guard.rs
|
@ -1,168 +1,232 @@
|
||||||
//! Route match guards.
|
//! Route guards.
|
||||||
//!
|
//!
|
||||||
//! Guards are one of the ways how actix-web router chooses a
|
//! Guards are used during routing to help select a matching service or handler using some aspect of
|
||||||
//! handler service. In essence it is just a function that accepts a
|
//! the request; though guards should not be used for path matching since it is a built-in function
|
||||||
//! reference to a `RequestHead` instance and returns a boolean.
|
//! of the Actix Web router.
|
||||||
//! It is possible to add guards to *scopes*, *resources*
|
|
||||||
//! and *routes*. Actix provide several guards by default, like various
|
|
||||||
//! http methods, header, etc. To become a guard, type must implement `Guard`
|
|
||||||
//! trait. Simple functions could be guards as well.
|
|
||||||
//!
|
//!
|
||||||
//! Guards can not modify the request object. But it is possible
|
//! Guards can be used on [`Scope`]s, [`Resource`]s, [`Route`]s, and other custom services.
|
||||||
//! to store extra attributes on a request by using the `Extensions` container.
|
|
||||||
//! Extensions containers are available via the `RequestHead::extensions()` method.
|
|
||||||
//!
|
//!
|
||||||
|
//! Fundamentally, a guard is a predicate function that receives a reference to a request context
|
||||||
|
//! object and returns a boolean; true if the request _should_ be handled by the guarded service
|
||||||
|
//! or handler. This interface is defined by the [`Guard`] trait.
|
||||||
|
//!
|
||||||
|
//! Commonly-used guards are provided in this module as well as a way of creating a guard from a
|
||||||
|
//! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be
|
||||||
|
//! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on
|
||||||
|
//! services multiple times (which might have different combining behavior than you want).
|
||||||
|
//!
|
||||||
|
//! There are shortcuts for routes with method guards in the [`web`](crate::web) module:
|
||||||
|
//! [`web::get()`](crate::web::get), [`web::post()`](crate::web::post), etc. The routes created by
|
||||||
|
//! the following calls are equivalent:
|
||||||
|
//! - `web::get()` (recommended form)
|
||||||
|
//! - `web::route().guard(guard::Get())`
|
||||||
|
//!
|
||||||
|
//! Guards can not modify anything about the request. However, it is possible to store extra
|
||||||
|
//! attributes in the request-local data container obtained with [`GuardContext::req_data_mut`].
|
||||||
|
//!
|
||||||
|
//! Guards can prevent resource definitions from overlapping which, when only considering paths,
|
||||||
|
//! would result in inaccessible routes. See the [`Host`] guard for an example of virtual hosting.
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//! In the following code, the `/guarded` resource has one defined route whose handler will only be
|
||||||
|
//! called if the request method is `POST` and there is a request header with name and value equal
|
||||||
|
//! to `x-guarded` and `secret`, respectively.
|
||||||
//! ```
|
//! ```
|
||||||
//! use actix_web::{web, http, dev, guard, App, HttpResponse};
|
//! use actix_web::{web, http::Method, guard, HttpResponse};
|
||||||
//!
|
//!
|
||||||
//! fn main() {
|
//! web::resource("/guarded").route(
|
||||||
//! App::new().service(web::resource("/index.html").route(
|
//! web::route()
|
||||||
//! web::route()
|
//! .guard(guard::Any(guard::Get()).or(guard::Post()))
|
||||||
//! .guard(guard::Post())
|
//! .guard(guard::Header("x-guarded", "secret"))
|
||||||
//! .guard(guard::fn_guard(|head| head.method == http::Method::GET))
|
//! .to(|| HttpResponse::Ok())
|
||||||
//! .to(|| HttpResponse::MethodNotAllowed()))
|
//! );
|
||||||
//! );
|
|
||||||
//! }
|
|
||||||
//! ```
|
//! ```
|
||||||
|
//!
|
||||||
|
//! [`Scope`]: crate::Scope::guard()
|
||||||
|
//! [`Resource`]: crate::Resource::guard()
|
||||||
|
//! [`Route`]: crate::Route::guard()
|
||||||
|
|
||||||
#![allow(non_snake_case)]
|
use std::{
|
||||||
|
cell::{Ref, RefMut},
|
||||||
|
convert::TryFrom,
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
use std::rc::Rc;
|
use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead};
|
||||||
use std::{convert::TryFrom, ops::Deref};
|
|
||||||
|
|
||||||
use actix_http::{header, uri::Uri, Method as HttpMethod, RequestHead};
|
use crate::service::ServiceRequest;
|
||||||
|
|
||||||
/// Trait defines resource guards. Guards are used for route selection.
|
/// Provides access to request parts that are useful during routing.
|
||||||
///
|
#[derive(Debug)]
|
||||||
/// Guards can not modify the request object. But it is possible
|
pub struct GuardContext<'a> {
|
||||||
/// to store extra attributes on a request by using the `Extensions` container.
|
pub(crate) req: &'a ServiceRequest,
|
||||||
/// Extensions containers are available via the `RequestHead::extensions()` method.
|
|
||||||
pub trait Guard {
|
|
||||||
/// Check if request matches predicate
|
|
||||||
fn check(&self, request: &RequestHead) -> bool;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Guard for Rc<dyn Guard> {
|
impl<'a> GuardContext<'a> {
|
||||||
fn check(&self, request: &RequestHead) -> bool {
|
/// Returns reference to the request head.
|
||||||
self.deref().check(request)
|
#[inline]
|
||||||
|
pub fn head(&self) -> &RequestHead {
|
||||||
|
self.req.head()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns reference to the request-local data container.
|
||||||
|
#[inline]
|
||||||
|
pub fn req_data(&self) -> Ref<'a, Extensions> {
|
||||||
|
self.req.req_data()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns mutable reference to the request-local data container.
|
||||||
|
#[inline]
|
||||||
|
pub fn req_data_mut(&self) -> RefMut<'a, Extensions> {
|
||||||
|
self.req.req_data_mut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create guard object for supplied function.
|
/// Interface for routing guards.
|
||||||
///
|
///
|
||||||
|
/// See [module level documentation](self) for more.
|
||||||
|
pub trait Guard {
|
||||||
|
/// Returns true if predicate condition is met for a given request.
|
||||||
|
fn check(&self, ctx: &GuardContext<'_>) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Guard for Rc<dyn Guard> {
|
||||||
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
|
(**self).check(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a guard using the given function.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{guard, web, App, HttpResponse};
|
/// use actix_web::{guard, web, HttpResponse};
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// web::route()
|
||||||
/// App::new().service(web::resource("/index.html").route(
|
/// .guard(guard::fn_guard(|ctx| {
|
||||||
/// web::route()
|
/// ctx.head().headers().contains_key("content-type")
|
||||||
/// .guard(
|
/// }))
|
||||||
/// guard::fn_guard(
|
/// .to(|| HttpResponse::Ok());
|
||||||
/// |req| req.headers()
|
|
||||||
/// .contains_key("content-type")))
|
|
||||||
/// .to(|| HttpResponse::MethodNotAllowed()))
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn fn_guard<F>(f: F) -> impl Guard
|
pub fn fn_guard<F>(f: F) -> impl Guard
|
||||||
where
|
where
|
||||||
F: Fn(&RequestHead) -> bool,
|
F: Fn(&GuardContext<'_>) -> bool,
|
||||||
{
|
{
|
||||||
FnGuard(f)
|
FnGuard(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FnGuard<F: Fn(&RequestHead) -> bool>(F);
|
struct FnGuard<F: Fn(&GuardContext<'_>) -> bool>(F);
|
||||||
|
|
||||||
impl<F> Guard for FnGuard<F>
|
impl<F> Guard for FnGuard<F>
|
||||||
where
|
where
|
||||||
F: Fn(&RequestHead) -> bool,
|
F: Fn(&GuardContext<'_>) -> bool,
|
||||||
{
|
{
|
||||||
fn check(&self, head: &RequestHead) -> bool {
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
(self.0)(head)
|
(self.0)(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F> Guard for F
|
impl<F> Guard for F
|
||||||
where
|
where
|
||||||
F: Fn(&RequestHead) -> bool,
|
F: Fn(&GuardContext<'_>) -> bool,
|
||||||
{
|
{
|
||||||
fn check(&self, head: &RequestHead) -> bool {
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
(self)(head)
|
(self)(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return guard that matches if any of supplied guards.
|
/// Creates a guard that matches if any added guards match.
|
||||||
///
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// The handler below will be called for either request method `GET` or `POST`.
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, guard, App, HttpResponse};
|
/// use actix_web::{web, guard, HttpResponse};
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// web::route()
|
||||||
/// App::new().service(web::resource("/index.html").route(
|
/// .guard(
|
||||||
/// web::route()
|
/// guard::Any(guard::Get())
|
||||||
/// .guard(guard::Any(guard::Get()).or(guard::Post()))
|
/// .or(guard::Post()))
|
||||||
/// .to(|| HttpResponse::MethodNotAllowed()))
|
/// .to(|| HttpResponse::Ok());
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
|
#[allow(non_snake_case)]
|
||||||
pub fn Any<F: Guard + 'static>(guard: F) -> AnyGuard {
|
pub fn Any<F: Guard + 'static>(guard: F) -> AnyGuard {
|
||||||
AnyGuard(vec![Box::new(guard)])
|
AnyGuard {
|
||||||
|
guards: vec![Box::new(guard)],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Matches any of supplied guards.
|
/// A collection of guards that match if the disjunction of their `check` outcomes is true.
|
||||||
pub struct AnyGuard(Vec<Box<dyn Guard>>);
|
///
|
||||||
|
/// That is, only one contained guard needs to match in order for the aggregate guard to match.
|
||||||
|
///
|
||||||
|
/// Construct an `AnyGuard` using [`Any`].
|
||||||
|
pub struct AnyGuard {
|
||||||
|
guards: Vec<Box<dyn Guard>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl AnyGuard {
|
impl AnyGuard {
|
||||||
/// Add guard to a list of guards to check
|
/// Adds new guard to the collection of guards to check.
|
||||||
pub fn or<F: Guard + 'static>(mut self, guard: F) -> Self {
|
pub fn or<F: Guard + 'static>(mut self, guard: F) -> Self {
|
||||||
self.0.push(Box::new(guard));
|
self.guards.push(Box::new(guard));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Guard for AnyGuard {
|
impl Guard for AnyGuard {
|
||||||
fn check(&self, req: &RequestHead) -> bool {
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
for p in &self.0 {
|
for guard in &self.guards {
|
||||||
if p.check(req) {
|
if guard.check(ctx) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return guard that matches if all of the supplied guards.
|
/// Creates a guard that matches if all added guards match.
|
||||||
///
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// The handler below will only be called if the request method is `GET` **and** the specified
|
||||||
|
/// header name and value match exactly.
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{guard, web, App, HttpResponse};
|
/// use actix_web::{guard, web, HttpResponse};
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// web::route()
|
||||||
/// App::new().service(web::resource("/index.html").route(
|
/// .guard(
|
||||||
/// web::route()
|
/// guard::All(guard::Get())
|
||||||
/// .guard(
|
/// .and(guard::Header("accept", "text/plain"))
|
||||||
/// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain")))
|
/// )
|
||||||
/// .to(|| HttpResponse::MethodNotAllowed()))
|
/// .to(|| HttpResponse::Ok());
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
|
#[allow(non_snake_case)]
|
||||||
pub fn All<F: Guard + 'static>(guard: F) -> AllGuard {
|
pub fn All<F: Guard + 'static>(guard: F) -> AllGuard {
|
||||||
AllGuard(vec![Box::new(guard)])
|
AllGuard {
|
||||||
|
guards: vec![Box::new(guard)],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Matches if all of supplied guards.
|
/// A collection of guards that match if the conjunction of their `check` outcomes is true.
|
||||||
pub struct AllGuard(Vec<Box<dyn Guard>>);
|
///
|
||||||
|
/// That is, **all** contained guard needs to match in order for the aggregate guard to match.
|
||||||
|
///
|
||||||
|
/// Construct an `AllGuard` using [`All`].
|
||||||
|
pub struct AllGuard {
|
||||||
|
guards: Vec<Box<dyn Guard>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl AllGuard {
|
impl AllGuard {
|
||||||
/// Add new guard to the list of guards to check
|
/// Adds new guard to the collection of guards to check.
|
||||||
pub fn and<F: Guard + 'static>(mut self, guard: F) -> Self {
|
pub fn and<F: Guard + 'static>(mut self, guard: F) -> Self {
|
||||||
self.0.push(Box::new(guard));
|
self.guards.push(Box::new(guard));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Guard for AllGuard {
|
impl Guard for AllGuard {
|
||||||
fn check(&self, request: &RequestHead) -> bool {
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
for p in &self.0 {
|
for guard in &self.guards {
|
||||||
if !p.check(request) {
|
if !guard.check(ctx) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,161 +234,212 @@ impl Guard for AllGuard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return guard that matches if supplied guard does not match.
|
/// Wraps a guard and inverts the outcome of it's `Guard` implementation.
|
||||||
pub fn Not<F: Guard + 'static>(guard: F) -> NotGuard {
|
///
|
||||||
NotGuard(Box::new(guard))
|
/// # Examples
|
||||||
}
|
/// The handler below will be called for any request method apart from `GET`.
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::{guard, web, HttpResponse};
|
||||||
|
///
|
||||||
|
/// web::route()
|
||||||
|
/// .guard(guard::Not(guard::Get()))
|
||||||
|
/// .to(|| HttpResponse::Ok());
|
||||||
|
/// ```
|
||||||
|
pub struct Not<G>(pub G);
|
||||||
|
|
||||||
#[doc(hidden)]
|
impl<G: Guard> Guard for Not<G> {
|
||||||
pub struct NotGuard(Box<dyn Guard>);
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
|
!self.0.check(ctx)
|
||||||
impl Guard for NotGuard {
|
|
||||||
fn check(&self, request: &RequestHead) -> bool {
|
|
||||||
!self.0.check(request)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HTTP method guard.
|
/// Creates a guard that matches a specified HTTP method.
|
||||||
#[doc(hidden)]
|
#[allow(non_snake_case)]
|
||||||
pub struct MethodGuard(HttpMethod);
|
pub fn Method(method: HttpMethod) -> impl Guard {
|
||||||
|
|
||||||
impl Guard for MethodGuard {
|
|
||||||
fn check(&self, request: &RequestHead) -> bool {
|
|
||||||
request.method == self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Guard to match *GET* HTTP method.
|
|
||||||
pub fn Get() -> MethodGuard {
|
|
||||||
MethodGuard(HttpMethod::GET)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Predicate to match *POST* HTTP method.
|
|
||||||
pub fn Post() -> MethodGuard {
|
|
||||||
MethodGuard(HttpMethod::POST)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Predicate to match *PUT* HTTP method.
|
|
||||||
pub fn Put() -> MethodGuard {
|
|
||||||
MethodGuard(HttpMethod::PUT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Predicate to match *DELETE* HTTP method.
|
|
||||||
pub fn Delete() -> MethodGuard {
|
|
||||||
MethodGuard(HttpMethod::DELETE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Predicate to match *HEAD* HTTP method.
|
|
||||||
pub fn Head() -> MethodGuard {
|
|
||||||
MethodGuard(HttpMethod::HEAD)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Predicate to match *OPTIONS* HTTP method.
|
|
||||||
pub fn Options() -> MethodGuard {
|
|
||||||
MethodGuard(HttpMethod::OPTIONS)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Predicate to match *CONNECT* HTTP method.
|
|
||||||
pub fn Connect() -> MethodGuard {
|
|
||||||
MethodGuard(HttpMethod::CONNECT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Predicate to match *PATCH* HTTP method.
|
|
||||||
pub fn Patch() -> MethodGuard {
|
|
||||||
MethodGuard(HttpMethod::PATCH)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Predicate to match *TRACE* HTTP method.
|
|
||||||
pub fn Trace() -> MethodGuard {
|
|
||||||
MethodGuard(HttpMethod::TRACE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Predicate to match specified HTTP method.
|
|
||||||
pub fn Method(method: HttpMethod) -> MethodGuard {
|
|
||||||
MethodGuard(method)
|
MethodGuard(method)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return predicate that matches if request contains specified header and
|
/// HTTP method guard.
|
||||||
/// value.
|
struct MethodGuard(HttpMethod);
|
||||||
pub fn Header(name: &'static str, value: &'static str) -> HeaderGuard {
|
|
||||||
|
impl Guard for MethodGuard {
|
||||||
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
|
ctx.head().method == self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! method_guard {
|
||||||
|
($method_fn:ident, $method_const:ident) => {
|
||||||
|
paste::paste! {
|
||||||
|
#[doc = " Creates a guard that matches the `" $method_const "` request method."]
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
#[doc = " The route in this example will only respond to `" $method_const "` requests."]
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::{guard, web, HttpResponse};
|
||||||
|
///
|
||||||
|
/// web::route()
|
||||||
|
#[doc = " .guard(guard::" $method_fn "())"]
|
||||||
|
/// .to(|| HttpResponse::Ok());
|
||||||
|
/// ```
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn $method_fn() -> impl Guard {
|
||||||
|
MethodGuard(HttpMethod::$method_const)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
method_guard!(Get, GET);
|
||||||
|
method_guard!(Post, POST);
|
||||||
|
method_guard!(Put, PUT);
|
||||||
|
method_guard!(Delete, DELETE);
|
||||||
|
method_guard!(Head, HEAD);
|
||||||
|
method_guard!(Options, OPTIONS);
|
||||||
|
method_guard!(Connect, CONNECT);
|
||||||
|
method_guard!(Patch, PATCH);
|
||||||
|
method_guard!(Trace, TRACE);
|
||||||
|
|
||||||
|
/// Creates a guard that matches if request contains given header name and value.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// The handler below will be called when the request contains an `x-guarded` header with value
|
||||||
|
/// equal to `secret`.
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::{guard, web, HttpResponse};
|
||||||
|
///
|
||||||
|
/// web::route()
|
||||||
|
/// .guard(guard::Header("x-guarded", "secret"))
|
||||||
|
/// .to(|| HttpResponse::Ok());
|
||||||
|
/// ```
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Header(name: &'static str, value: &'static str) -> impl Guard {
|
||||||
HeaderGuard(
|
HeaderGuard(
|
||||||
header::HeaderName::try_from(name).unwrap(),
|
header::HeaderName::try_from(name).unwrap(),
|
||||||
header::HeaderValue::from_static(value),
|
header::HeaderValue::from_static(value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
struct HeaderGuard(header::HeaderName, header::HeaderValue);
|
||||||
pub struct HeaderGuard(header::HeaderName, header::HeaderValue);
|
|
||||||
|
|
||||||
impl Guard for HeaderGuard {
|
impl Guard for HeaderGuard {
|
||||||
fn check(&self, req: &RequestHead) -> bool {
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
if let Some(val) = req.headers.get(&self.0) {
|
if let Some(val) = ctx.head().headers.get(&self.0) {
|
||||||
return val == self.1;
|
return val == self.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return predicate that matches if request contains specified Host name.
|
/// Creates a guard that matches requests targetting a specific host.
|
||||||
///
|
///
|
||||||
|
/// # Matching Host
|
||||||
|
/// This guard will:
|
||||||
|
/// - match against the `Host` header, if present;
|
||||||
|
/// - fall-back to matching against the request target's host, if present;
|
||||||
|
/// - return false if host cannot be determined;
|
||||||
|
///
|
||||||
|
/// # Matching Scheme
|
||||||
|
/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using
|
||||||
|
/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent
|
||||||
|
/// the guard from matching successfully.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards.
|
||||||
|
///
|
||||||
|
/// The example below additionally guards on the host URI's scheme. This could allow routing to
|
||||||
|
/// different handlers for `http:` vs `https:` visitors; to redirect, for example.
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, guard::Host, App, HttpResponse};
|
/// use actix_web::{web, guard::Host, HttpResponse};
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// web::scope("/admin")
|
||||||
/// App::new().service(
|
/// .guard(Host("admin.rust-lang.org").scheme("https"))
|
||||||
/// web::resource("/index.html")
|
/// .default_service(web::to(|| HttpResponse::Ok().body("admin connection is secure")));
|
||||||
/// .guard(Host("www.rust-lang.org"))
|
/// ```
|
||||||
/// .to(|| HttpResponse::MethodNotAllowed())
|
///
|
||||||
|
/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app.
|
||||||
|
/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard
|
||||||
|
/// definitions they become safe to use in this way. Without these host guards, only routes under
|
||||||
|
/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1`
|
||||||
|
/// and `localhost` as the `Host` guards.
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::{web, http::Method, guard, App, HttpResponse};
|
||||||
|
///
|
||||||
|
/// App::new()
|
||||||
|
/// .service(
|
||||||
|
/// web::scope("")
|
||||||
|
/// .guard(guard::Host("www.rust-lang.org"))
|
||||||
|
/// .default_service(web::to(|| HttpResponse::Ok().body("marketing site"))),
|
||||||
|
/// )
|
||||||
|
/// .service(
|
||||||
|
/// web::scope("")
|
||||||
|
/// .guard(guard::Host("play.rust-lang.org"))
|
||||||
|
/// .default_service(web::to(|| HttpResponse::Ok().body("playground frontend"))),
|
||||||
/// );
|
/// );
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn Host<H: AsRef<str>>(host: H) -> HostGuard {
|
///
|
||||||
HostGuard(host.as_ref().to_string(), None)
|
/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Host(host: impl AsRef<str>) -> HostGuard {
|
||||||
|
HostGuard {
|
||||||
|
host: host.as_ref().to_string(),
|
||||||
|
scheme: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_host_uri(req: &RequestHead) -> Option<Uri> {
|
fn get_host_uri(req: &RequestHead) -> Option<Uri> {
|
||||||
use core::str::FromStr;
|
|
||||||
req.headers
|
req.headers
|
||||||
.get(header::HOST)
|
.get(header::HOST)
|
||||||
.and_then(|host_value| host_value.to_str().ok())
|
.and_then(|host_value| host_value.to_str().ok())
|
||||||
.or_else(|| req.uri.host())
|
.or_else(|| req.uri.host())
|
||||||
.map(|host: &str| Uri::from_str(host).ok())
|
.and_then(|host| host.parse().ok())
|
||||||
.and_then(|host_success| host_success)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub struct HostGuard(String, Option<String>);
|
pub struct HostGuard {
|
||||||
|
host: String,
|
||||||
|
scheme: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl HostGuard {
|
impl HostGuard {
|
||||||
/// Set request scheme to match
|
/// Set request scheme to match
|
||||||
pub fn scheme<H: AsRef<str>>(mut self, scheme: H) -> HostGuard {
|
pub fn scheme<H: AsRef<str>>(mut self, scheme: H) -> HostGuard {
|
||||||
self.1 = Some(scheme.as_ref().to_string());
|
self.scheme = Some(scheme.as_ref().to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Guard for HostGuard {
|
impl Guard for HostGuard {
|
||||||
fn check(&self, req: &RequestHead) -> bool {
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
let req_host_uri = if let Some(uri) = get_host_uri(req) {
|
// parse host URI from header or request target
|
||||||
uri
|
let req_host_uri = match get_host_uri(ctx.head()) {
|
||||||
} else {
|
Some(uri) => uri,
|
||||||
return false;
|
|
||||||
|
// no match if host cannot be determined
|
||||||
|
None => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(uri_host) = req_host_uri.host() {
|
match req_host_uri.host() {
|
||||||
if self.0 != uri_host {
|
// fall through to scheme checks
|
||||||
return false;
|
Some(uri_host) if self.host == uri_host => {}
|
||||||
}
|
|
||||||
} else {
|
// Either:
|
||||||
return false;
|
// - request's host does not match guard's host;
|
||||||
|
// - It was possible that the parsed URI from request target did not contain a host.
|
||||||
|
_ => return false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref scheme) = self.1 {
|
if let Some(ref scheme) = self.scheme {
|
||||||
if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() {
|
if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() {
|
||||||
return scheme == req_host_uri_scheme;
|
return scheme == req_host_uri_scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: is the the correct behavior?
|
||||||
|
// falls through if scheme cannot be determined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// all conditions passed
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -337,171 +452,214 @@ mod tests {
|
||||||
use crate::test::TestRequest;
|
use crate::test::TestRequest;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_header() {
|
fn header_match() {
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.insert_header((header::TRANSFER_ENCODING, "chunked"))
|
.insert_header((header::TRANSFER_ENCODING, "chunked"))
|
||||||
.to_http_request();
|
.to_srv_request();
|
||||||
|
|
||||||
let pred = Header("transfer-encoding", "chunked");
|
let hdr = Header("transfer-encoding", "chunked");
|
||||||
assert!(pred.check(req.head()));
|
assert!(hdr.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Header("transfer-encoding", "other");
|
let hdr = Header("transfer-encoding", "other");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!hdr.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Header("content-type", "other");
|
let hdr = Header("content-type", "chunked");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!hdr.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let hdr = Header("content-type", "other");
|
||||||
|
assert!(!hdr.check(&req.guard_ctx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_host() {
|
fn host_from_header() {
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.insert_header((
|
.insert_header((
|
||||||
header::HOST,
|
header::HOST,
|
||||||
header::HeaderValue::from_static("www.rust-lang.org"),
|
header::HeaderValue::from_static("www.rust-lang.org"),
|
||||||
))
|
))
|
||||||
.to_http_request();
|
.to_srv_request();
|
||||||
|
|
||||||
let pred = Host("www.rust-lang.org");
|
let host = Host("www.rust-lang.org");
|
||||||
assert!(pred.check(req.head()));
|
assert!(host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("www.rust-lang.org").scheme("https");
|
let host = Host("www.rust-lang.org").scheme("https");
|
||||||
assert!(pred.check(req.head()));
|
assert!(host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("blog.rust-lang.org");
|
let host = Host("blog.rust-lang.org");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("blog.rust-lang.org").scheme("https");
|
let host = Host("blog.rust-lang.org").scheme("https");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("crates.io");
|
let host = Host("crates.io");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("localhost");
|
let host = Host("localhost");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_host_scheme() {
|
fn host_without_header() {
|
||||||
|
let req = TestRequest::default()
|
||||||
|
.uri("www.rust-lang.org")
|
||||||
|
.to_srv_request();
|
||||||
|
|
||||||
|
let host = Host("www.rust-lang.org");
|
||||||
|
assert!(host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let host = Host("www.rust-lang.org").scheme("https");
|
||||||
|
assert!(host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let host = Host("blog.rust-lang.org");
|
||||||
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let host = Host("blog.rust-lang.org").scheme("https");
|
||||||
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let host = Host("crates.io");
|
||||||
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let host = Host("localhost");
|
||||||
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_scheme() {
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.insert_header((
|
.insert_header((
|
||||||
header::HOST,
|
header::HOST,
|
||||||
header::HeaderValue::from_static("https://www.rust-lang.org"),
|
header::HeaderValue::from_static("https://www.rust-lang.org"),
|
||||||
))
|
))
|
||||||
.to_http_request();
|
.to_srv_request();
|
||||||
|
|
||||||
let pred = Host("www.rust-lang.org").scheme("https");
|
let host = Host("www.rust-lang.org").scheme("https");
|
||||||
assert!(pred.check(req.head()));
|
assert!(host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("www.rust-lang.org");
|
let host = Host("www.rust-lang.org");
|
||||||
assert!(pred.check(req.head()));
|
assert!(host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("www.rust-lang.org").scheme("http");
|
let host = Host("www.rust-lang.org").scheme("http");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("blog.rust-lang.org");
|
let host = Host("blog.rust-lang.org");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("blog.rust-lang.org").scheme("https");
|
let host = Host("blog.rust-lang.org").scheme("https");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("crates.io").scheme("https");
|
let host = Host("crates.io").scheme("https");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
|
|
||||||
let pred = Host("localhost");
|
let host = Host("localhost");
|
||||||
assert!(!pred.check(req.head()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_host_without_header() {
|
fn method_guards() {
|
||||||
|
let get_req = TestRequest::get().to_srv_request();
|
||||||
|
let post_req = TestRequest::post().to_srv_request();
|
||||||
|
|
||||||
|
assert!(Get().check(&get_req.guard_ctx()));
|
||||||
|
assert!(!Get().check(&post_req.guard_ctx()));
|
||||||
|
|
||||||
|
assert!(Post().check(&post_req.guard_ctx()));
|
||||||
|
assert!(!Post().check(&get_req.guard_ctx()));
|
||||||
|
|
||||||
|
let req = TestRequest::put().to_srv_request();
|
||||||
|
assert!(Put().check(&req.guard_ctx()));
|
||||||
|
assert!(!Put().check(&get_req.guard_ctx()));
|
||||||
|
|
||||||
|
let req = TestRequest::patch().to_srv_request();
|
||||||
|
assert!(Patch().check(&req.guard_ctx()));
|
||||||
|
assert!(!Patch().check(&get_req.guard_ctx()));
|
||||||
|
|
||||||
|
let r = TestRequest::delete().to_srv_request();
|
||||||
|
assert!(Delete().check(&r.guard_ctx()));
|
||||||
|
assert!(!Delete().check(&get_req.guard_ctx()));
|
||||||
|
|
||||||
|
let req = TestRequest::default().method(Method::HEAD).to_srv_request();
|
||||||
|
assert!(Head().check(&req.guard_ctx()));
|
||||||
|
assert!(!Head().check(&get_req.guard_ctx()));
|
||||||
|
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.uri("www.rust-lang.org")
|
|
||||||
.to_http_request();
|
|
||||||
|
|
||||||
let pred = Host("www.rust-lang.org");
|
|
||||||
assert!(pred.check(req.head()));
|
|
||||||
|
|
||||||
let pred = Host("www.rust-lang.org").scheme("https");
|
|
||||||
assert!(pred.check(req.head()));
|
|
||||||
|
|
||||||
let pred = Host("blog.rust-lang.org");
|
|
||||||
assert!(!pred.check(req.head()));
|
|
||||||
|
|
||||||
let pred = Host("blog.rust-lang.org").scheme("https");
|
|
||||||
assert!(!pred.check(req.head()));
|
|
||||||
|
|
||||||
let pred = Host("crates.io");
|
|
||||||
assert!(!pred.check(req.head()));
|
|
||||||
|
|
||||||
let pred = Host("localhost");
|
|
||||||
assert!(!pred.check(req.head()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_methods() {
|
|
||||||
let req = TestRequest::default().to_http_request();
|
|
||||||
let req2 = TestRequest::default()
|
|
||||||
.method(Method::POST)
|
|
||||||
.to_http_request();
|
|
||||||
|
|
||||||
assert!(Get().check(req.head()));
|
|
||||||
assert!(!Get().check(req2.head()));
|
|
||||||
assert!(Post().check(req2.head()));
|
|
||||||
assert!(!Post().check(req.head()));
|
|
||||||
|
|
||||||
let r = TestRequest::default().method(Method::PUT).to_http_request();
|
|
||||||
assert!(Put().check(r.head()));
|
|
||||||
assert!(!Put().check(req.head()));
|
|
||||||
|
|
||||||
let r = TestRequest::default()
|
|
||||||
.method(Method::DELETE)
|
|
||||||
.to_http_request();
|
|
||||||
assert!(Delete().check(r.head()));
|
|
||||||
assert!(!Delete().check(req.head()));
|
|
||||||
|
|
||||||
let r = TestRequest::default()
|
|
||||||
.method(Method::HEAD)
|
|
||||||
.to_http_request();
|
|
||||||
assert!(Head().check(r.head()));
|
|
||||||
assert!(!Head().check(req.head()));
|
|
||||||
|
|
||||||
let r = TestRequest::default()
|
|
||||||
.method(Method::OPTIONS)
|
.method(Method::OPTIONS)
|
||||||
.to_http_request();
|
.to_srv_request();
|
||||||
assert!(Options().check(r.head()));
|
assert!(Options().check(&req.guard_ctx()));
|
||||||
assert!(!Options().check(req.head()));
|
assert!(!Options().check(&get_req.guard_ctx()));
|
||||||
|
|
||||||
let r = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.method(Method::CONNECT)
|
.method(Method::CONNECT)
|
||||||
.to_http_request();
|
.to_srv_request();
|
||||||
assert!(Connect().check(r.head()));
|
assert!(Connect().check(&req.guard_ctx()));
|
||||||
assert!(!Connect().check(req.head()));
|
assert!(!Connect().check(&get_req.guard_ctx()));
|
||||||
|
|
||||||
let r = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.method(Method::PATCH)
|
|
||||||
.to_http_request();
|
|
||||||
assert!(Patch().check(r.head()));
|
|
||||||
assert!(!Patch().check(req.head()));
|
|
||||||
|
|
||||||
let r = TestRequest::default()
|
|
||||||
.method(Method::TRACE)
|
.method(Method::TRACE)
|
||||||
.to_http_request();
|
.to_srv_request();
|
||||||
assert!(Trace().check(r.head()));
|
assert!(Trace().check(&req.guard_ctx()));
|
||||||
assert!(!Trace().check(req.head()));
|
assert!(!Trace().check(&get_req.guard_ctx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_preds() {
|
fn aggregate_any() {
|
||||||
let r = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.method(Method::TRACE)
|
.method(Method::TRACE)
|
||||||
.to_http_request();
|
.to_srv_request();
|
||||||
|
|
||||||
assert!(Not(Get()).check(r.head()));
|
assert!(Any(Trace()).check(&req.guard_ctx()));
|
||||||
assert!(!Not(Trace()).check(r.head()));
|
assert!(Any(Trace()).or(Get()).check(&req.guard_ctx()));
|
||||||
|
assert!(!Any(Get()).or(Get()).check(&req.guard_ctx()));
|
||||||
|
}
|
||||||
|
|
||||||
assert!(All(Trace()).and(Trace()).check(r.head()));
|
#[test]
|
||||||
assert!(!All(Get()).and(Trace()).check(r.head()));
|
fn aggregate_all() {
|
||||||
|
let req = TestRequest::default()
|
||||||
|
.method(Method::TRACE)
|
||||||
|
.to_srv_request();
|
||||||
|
|
||||||
assert!(Any(Get()).or(Trace()).check(r.head()));
|
assert!(All(Trace()).check(&req.guard_ctx()));
|
||||||
assert!(!Any(Get()).or(Get()).check(r.head()));
|
assert!(All(Trace()).and(Trace()).check(&req.guard_ctx()));
|
||||||
|
assert!(!All(Trace()).and(Get()).check(&req.guard_ctx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_not() {
|
||||||
|
let req = TestRequest::default().to_srv_request();
|
||||||
|
|
||||||
|
let get = Get();
|
||||||
|
assert!(get.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let not_get = Not(get);
|
||||||
|
assert!(!not_get.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let not_not_get = Not(not_get);
|
||||||
|
assert!(not_not_get.check(&req.guard_ctx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_guard() {
|
||||||
|
let domain = "rust-lang.org".to_owned();
|
||||||
|
let guard = fn_guard(|ctx| ctx.head().uri.host().unwrap().ends_with(&domain));
|
||||||
|
|
||||||
|
let req = TestRequest::default()
|
||||||
|
.uri("blog.rust-lang.org")
|
||||||
|
.to_srv_request();
|
||||||
|
assert!(guard.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let req = TestRequest::default().uri("crates.io").to_srv_request();
|
||||||
|
assert!(!guard.check(&req.guard_ctx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mega_nesting() {
|
||||||
|
let guard = fn_guard(|ctx| All(Not(Any(Not(Trace())))).check(ctx));
|
||||||
|
|
||||||
|
let req = TestRequest::default().to_srv_request();
|
||||||
|
assert!(!guard.check(&req.guard_ctx()));
|
||||||
|
|
||||||
|
let req = TestRequest::default()
|
||||||
|
.method(Method::TRACE)
|
||||||
|
.to_srv_request();
|
||||||
|
assert!(guard.check(&req.guard_ctx()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
118
src/handler.rs
118
src/handler.rs
|
@ -3,35 +3,92 @@ use std::future::Future;
|
||||||
use actix_service::{boxed, fn_service};
|
use actix_service::{boxed, fn_service};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::MessageBody,
|
|
||||||
service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse},
|
service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse},
|
||||||
BoxError, FromRequest, HttpResponse, Responder,
|
FromRequest, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A request handler is an async function that accepts zero or more parameters that can be
|
/// The interface for request handlers.
|
||||||
/// extracted from a request (i.e., [`impl FromRequest`]) and returns a type that can be converted
|
|
||||||
/// into an [`HttpResponse`] (that is, it impls the [`Responder`] trait).
|
|
||||||
///
|
///
|
||||||
/// If you got the error `the trait Handler<_, _, _> is not implemented`, then your function is not
|
/// # What Is A Request Handler
|
||||||
/// a valid handler. See <https://actix.rs/docs/handlers> for more information.
|
/// A request handler has three requirements:
|
||||||
|
/// 1. It is an async function (or a function/closure that returns an appropriate future);
|
||||||
|
/// 1. The function accepts zero or more parameters that implement [`FromRequest`];
|
||||||
|
/// 1. The async function (or future) resolves to a type that can be converted into an
|
||||||
|
/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait).
|
||||||
///
|
///
|
||||||
/// [`impl FromRequest`]: crate::FromRequest
|
/// # Compiler Errors
|
||||||
pub trait Handler<T, R>: Clone + 'static
|
/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not
|
||||||
where
|
/// fulfill one or more of the above requirements.
|
||||||
R: Future,
|
///
|
||||||
R::Output: Responder,
|
/// Unfortunately we cannot provide a better compile error message (while keeping the trait's
|
||||||
{
|
/// flexibility) unless a stable alternative to [`#[rustc_on_unimplemented]`][on_unimpl] is added
|
||||||
fn call(&self, param: T) -> R;
|
/// to Rust.
|
||||||
|
///
|
||||||
|
/// # How Do Handlers Receive Variable Numbers Of Arguments
|
||||||
|
/// Rest assured there is no macro magic here; it's just traits.
|
||||||
|
///
|
||||||
|
/// The first thing to note is that [`FromRequest`] is implemented for tuples (up to 12 in length).
|
||||||
|
///
|
||||||
|
/// Secondly, the `Handler` trait is implemented for functions (up to an [arity] of 12) in a way
|
||||||
|
/// that aligns their parameter positions with a corresponding tuple of types (becoming the `Args`
|
||||||
|
/// type parameter for this trait).
|
||||||
|
///
|
||||||
|
/// Thanks to Rust's type system, Actix Web can infer the function parameter types. During the
|
||||||
|
/// extraction step, the parameter types are described as a tuple type, [`from_request`] is run on
|
||||||
|
/// that tuple, and the `Handler::call` implementation for that particular function arity
|
||||||
|
/// destructures the tuple into it's component types and calls your handler function with them.
|
||||||
|
///
|
||||||
|
/// In pseudo-code the process looks something like this:
|
||||||
|
/// ```ignore
|
||||||
|
/// async fn my_handler(body: String, state: web::Data<MyState>) -> impl Responder {
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // the function params above described as a tuple, names do not matter, only position
|
||||||
|
/// type InferredMyHandlerArgs = (String, web::Data<MyState>);
|
||||||
|
///
|
||||||
|
/// // create tuple of arguments to be passed to handler
|
||||||
|
/// let args = InferredMyHandlerArgs::from_request(&request, &payload).await;
|
||||||
|
///
|
||||||
|
/// // call handler with argument tuple
|
||||||
|
/// let response = Handler::call(&my_handler, args).await;
|
||||||
|
///
|
||||||
|
/// // which is effectively...
|
||||||
|
///
|
||||||
|
/// let (body, state) = args;
|
||||||
|
/// let response = my_handler(body, state).await;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the
|
||||||
|
/// bounds of the handler call after argument extraction:
|
||||||
|
/// ```ignore
|
||||||
|
/// impl<Func, Arg1, Arg2, R> Handler<(Arg1, Arg2), R> for Func
|
||||||
|
/// where
|
||||||
|
/// Func: Fn(Arg1, Arg2) -> R + Clone + 'static,
|
||||||
|
/// R: Future,
|
||||||
|
/// R::Output: Responder,
|
||||||
|
/// {
|
||||||
|
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> R {
|
||||||
|
/// (self)(arg1, arg2)
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [arity]: https://en.wikipedia.org/wiki/Arity
|
||||||
|
/// [`from_request`]: FromRequest::from_request
|
||||||
|
/// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628
|
||||||
|
pub trait Handler<Args>: Clone + 'static {
|
||||||
|
type Output;
|
||||||
|
type Future: Future<Output = Self::Output>;
|
||||||
|
|
||||||
|
fn call(&self, args: Args) -> Self::Future;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handler_service<F, T, R>(handler: F) -> BoxedHttpServiceFactory
|
pub(crate) fn handler_service<F, Args>(handler: F) -> BoxedHttpServiceFactory
|
||||||
where
|
where
|
||||||
F: Handler<T, R>,
|
F: Handler<Args>,
|
||||||
T: FromRequest,
|
Args: FromRequest,
|
||||||
R: Future,
|
F::Output: Responder,
|
||||||
R::Output: Responder,
|
|
||||||
<R::Output as Responder>::Body: MessageBody,
|
|
||||||
<<R::Output as Responder>::Body as MessageBody>::Error: Into<BoxError>,
|
|
||||||
{
|
{
|
||||||
boxed::factory(fn_service(move |req: ServiceRequest| {
|
boxed::factory(fn_service(move |req: ServiceRequest| {
|
||||||
let handler = handler.clone();
|
let handler = handler.clone();
|
||||||
|
@ -39,7 +96,7 @@ where
|
||||||
async move {
|
async move {
|
||||||
let (req, mut payload) = req.into_parts();
|
let (req, mut payload) = req.into_parts();
|
||||||
|
|
||||||
let res = match T::from_request(&req, &mut payload).await {
|
let res = match Args::from_request(&req, &mut payload).await {
|
||||||
Err(err) => HttpResponse::from_error(err),
|
Err(err) => HttpResponse::from_error(err),
|
||||||
|
|
||||||
Ok(data) => handler
|
Ok(data) => handler
|
||||||
|
@ -59,17 +116,20 @@ where
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// factory_tuple! {} // implements Handler for types: fn() -> Res
|
/// factory_tuple! {} // implements Handler for types: fn() -> R
|
||||||
/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> Res
|
/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> R
|
||||||
/// ```
|
/// ```
|
||||||
macro_rules! factory_tuple ({ $($param:ident)* } => {
|
macro_rules! factory_tuple ({ $($param:ident)* } => {
|
||||||
impl<Func, $($param,)* Res> Handler<($($param,)*), Res> for Func
|
impl<Func, Fut, $($param,)*> Handler<($($param,)*)> for Func
|
||||||
where Func: Fn($($param),*) -> Res + Clone + 'static,
|
where Func: Fn($($param),*) -> Fut + Clone + 'static,
|
||||||
Res: Future,
|
Fut: Future,
|
||||||
Res::Output: Responder,
|
|
||||||
{
|
{
|
||||||
|
type Output = Fut::Output;
|
||||||
|
type Future = Fut;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn call(&self, ($($param,)*): ($($param,)*)) -> Res {
|
fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future {
|
||||||
(self)($($param,)*)
|
(self)($($param,)*)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
|
|
||||||
pub mod header;
|
pub mod header;
|
||||||
|
|
||||||
|
// TODO: figure out how best to expose http::Error vs actix_http::Error
|
||||||
pub use actix_http::{uri, ConnectionType, Error, Method, StatusCode, Uri, Version};
|
pub use actix_http::{uri, ConnectionType, Error, Method, StatusCode, Uri, Version};
|
||||||
|
|
125
src/info.rs
125
src/info.rs
|
@ -67,7 +67,7 @@ fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<
|
||||||
pub struct ConnectionInfo {
|
pub struct ConnectionInfo {
|
||||||
host: String,
|
host: String,
|
||||||
scheme: String,
|
scheme: String,
|
||||||
remote_addr: Option<String>,
|
peer_addr: Option<String>,
|
||||||
realip_remote_addr: Option<String>,
|
realip_remote_addr: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,67 +134,70 @@ impl ConnectionInfo {
|
||||||
.or_else(|| first_header_value(req, &*X_FORWARDED_FOR))
|
.or_else(|| first_header_value(req, &*X_FORWARDED_FOR))
|
||||||
.map(str::to_owned);
|
.map(str::to_owned);
|
||||||
|
|
||||||
let remote_addr = req.peer_addr.map(|addr| addr.to_string());
|
let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string());
|
||||||
|
|
||||||
ConnectionInfo {
|
ConnectionInfo {
|
||||||
host,
|
host,
|
||||||
scheme,
|
scheme,
|
||||||
remote_addr,
|
peer_addr,
|
||||||
realip_remote_addr,
|
realip_remote_addr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Real IP (remote address) of client that initiated request.
|
||||||
|
///
|
||||||
|
/// The address is resolved through the following, in order:
|
||||||
|
/// - `Forwarded` header
|
||||||
|
/// - `X-Forwarded-For` header
|
||||||
|
/// - peer address of opened socket (same as [`remote_addr`](Self::remote_addr))
|
||||||
|
///
|
||||||
|
/// # Security
|
||||||
|
/// Do not use this function for security purposes unless you can be sure that the `Forwarded`
|
||||||
|
/// and `X-Forwarded-For` headers cannot be spoofed by the client. If you are running without a
|
||||||
|
/// proxy then [obtaining the peer address](Self::peer_addr) would be more appropriate.
|
||||||
|
#[inline]
|
||||||
|
pub fn realip_remote_addr(&self) -> Option<&str> {
|
||||||
|
self.realip_remote_addr
|
||||||
|
.as_deref()
|
||||||
|
.or_else(|| self.peer_addr.as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns serialized IP address of the peer connection.
|
||||||
|
///
|
||||||
|
/// See [`HttpRequest::peer_addr`] for more details.
|
||||||
|
#[inline]
|
||||||
|
pub fn peer_addr(&self) -> Option<&str> {
|
||||||
|
self.peer_addr.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hostname of the request.
|
||||||
|
///
|
||||||
|
/// Hostname is resolved through the following, in order:
|
||||||
|
/// - `Forwarded` header
|
||||||
|
/// - `X-Forwarded-Host` header
|
||||||
|
/// - `Host` header
|
||||||
|
/// - request target / URI
|
||||||
|
/// - configured server hostname
|
||||||
|
#[inline]
|
||||||
|
pub fn host(&self) -> &str {
|
||||||
|
&self.host
|
||||||
|
}
|
||||||
|
|
||||||
/// Scheme of the request.
|
/// Scheme of the request.
|
||||||
///
|
///
|
||||||
/// Scheme is resolved through the following headers, in this order:
|
/// Scheme is resolved through the following, in order:
|
||||||
///
|
/// - `Forwarded` header
|
||||||
/// - Forwarded
|
/// - `X-Forwarded-Proto` header
|
||||||
/// - X-Forwarded-Proto
|
/// - request target / URI
|
||||||
/// - Uri
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn scheme(&self) -> &str {
|
pub fn scheme(&self) -> &str {
|
||||||
&self.scheme
|
&self.scheme
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hostname of the request.
|
#[doc(hidden)]
|
||||||
///
|
#[deprecated(since = "4.0.0", note = "Renamed to `peer_addr`.")]
|
||||||
/// Hostname is resolved through the following headers, in this order:
|
|
||||||
///
|
|
||||||
/// - Forwarded
|
|
||||||
/// - X-Forwarded-Host
|
|
||||||
/// - Host
|
|
||||||
/// - Uri
|
|
||||||
/// - Server hostname
|
|
||||||
pub fn host(&self) -> &str {
|
|
||||||
&self.host
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remote address of the connection.
|
|
||||||
///
|
|
||||||
/// Get remote_addr address from socket address.
|
|
||||||
pub fn remote_addr(&self) -> Option<&str> {
|
pub fn remote_addr(&self) -> Option<&str> {
|
||||||
self.remote_addr.as_deref()
|
self.peer_addr()
|
||||||
}
|
|
||||||
|
|
||||||
/// Real IP (remote address) of client that initiated request.
|
|
||||||
///
|
|
||||||
/// The address is resolved through the following headers, in this order:
|
|
||||||
///
|
|
||||||
/// - Forwarded
|
|
||||||
/// - X-Forwarded-For
|
|
||||||
/// - remote_addr name of opened socket
|
|
||||||
///
|
|
||||||
/// # Security
|
|
||||||
/// Do not use this function for security purposes, unless you can ensure the Forwarded and
|
|
||||||
/// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket
|
|
||||||
/// address explicitly, use [`HttpRequest::peer_addr()`][peer_addr] instead.
|
|
||||||
///
|
|
||||||
/// [peer_addr]: crate::web::HttpRequest::peer_addr()
|
|
||||||
#[inline]
|
|
||||||
pub fn realip_remote_addr(&self) -> Option<&str> {
|
|
||||||
self.realip_remote_addr
|
|
||||||
.as_deref()
|
|
||||||
.or_else(|| self.remote_addr.as_deref())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,7 +212,7 @@ impl FromRequest for ConnectionInfo {
|
||||||
|
|
||||||
/// Extractor for peer's socket address.
|
/// Extractor for peer's socket address.
|
||||||
///
|
///
|
||||||
/// Also see [`HttpRequest::peer_addr`].
|
/// Also see [`HttpRequest::peer_addr`] and [`ConnectionInfo::peer_addr`].
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -432,13 +435,37 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn peer_addr_extract() {
|
async fn peer_addr_extract() {
|
||||||
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res = PeerAddr::extract(&req).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||||
let req = TestRequest::default().peer_addr(addr).to_http_request();
|
let req = TestRequest::default().peer_addr(addr).to_http_request();
|
||||||
let peer_addr = PeerAddr::extract(&req).await.unwrap();
|
let peer_addr = PeerAddr::extract(&req).await.unwrap();
|
||||||
assert_eq!(peer_addr, PeerAddr(addr));
|
assert_eq!(peer_addr, PeerAddr(addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn remote_address() {
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let res = PeerAddr::extract(&req).await;
|
let res = ConnectionInfo::extract(&req).await.unwrap();
|
||||||
assert!(res.is_err());
|
assert!(res.peer_addr().is_none());
|
||||||
|
|
||||||
|
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||||
|
let req = TestRequest::default().peer_addr(addr).to_http_request();
|
||||||
|
let conn_info = ConnectionInfo::extract(&req).await.unwrap();
|
||||||
|
assert_eq!(conn_info.peer_addr().unwrap(), "127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn real_ip_from_socket_addr() {
|
||||||
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res = ConnectionInfo::extract(&req).await.unwrap();
|
||||||
|
assert!(res.realip_remote_addr().is_none());
|
||||||
|
|
||||||
|
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||||
|
let req = TestRequest::default().peer_addr(addr).to_http_request();
|
||||||
|
let conn_info = ConnectionInfo::extract(&req).await.unwrap();
|
||||||
|
assert_eq!(conn_info.realip_remote_addr().unwrap(), "127.0.0.1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,6 @@
|
||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
#![allow(clippy::needless_doctest_main, clippy::type_complexity)]
|
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
|
||||||
|
@ -106,6 +105,7 @@ pub use cookie;
|
||||||
pub use crate::app::App;
|
pub use crate::app::App;
|
||||||
pub use crate::error::{Error, ResponseError, Result};
|
pub use crate::error::{Error, ResponseError, Result};
|
||||||
pub use crate::extract::FromRequest;
|
pub use crate::extract::FromRequest;
|
||||||
|
pub use crate::handler::Handler;
|
||||||
pub use crate::request::HttpRequest;
|
pub use crate::request::HttpRequest;
|
||||||
pub use crate::resource::Resource;
|
pub use crate::resource::Resource;
|
||||||
pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder};
|
pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder};
|
||||||
|
|
|
@ -17,7 +17,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap),
|
/// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap),
|
||||||
/// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition).
|
/// and [`Condition`](super::Condition).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -38,6 +38,15 @@ pub struct Compat<T> {
|
||||||
transform: T,
|
transform: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl Compat<super::Noop> {
|
||||||
|
pub(crate) fn noop() -> Self {
|
||||||
|
Self {
|
||||||
|
transform: super::Noop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Compat<T> {
|
impl<T> Compat<T> {
|
||||||
/// Wrap a middleware to give it broader compatibility.
|
/// Wrap a middleware to give it broader compatibility.
|
||||||
pub fn new(middleware: T) -> Self {
|
pub fn new(middleware: T) -> Self {
|
||||||
|
|
|
@ -113,6 +113,7 @@ where
|
||||||
{
|
{
|
||||||
type Response = ServiceResponse<EitherBody<Encoder<B>>>;
|
type Response = ServiceResponse<EitherBody<Encoder<B>>>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
type Future = Either<CompressResponse<S, B>, Ready<Result<Self::Response, Self::Error>>>;
|
type Future = Either<CompressResponse<S, B>, Ready<Result<Self::Response, Self::Error>>>;
|
||||||
|
|
||||||
actix_service::forward_ready!(service);
|
actix_service::forward_ready!(service);
|
||||||
|
|
|
@ -100,10 +100,9 @@ impl DefaultHeaders {
|
||||||
///
|
///
|
||||||
/// Default is `application/octet-stream`.
|
/// Default is `application/octet-stream`.
|
||||||
pub fn add_content_type(self) -> Self {
|
pub fn add_content_type(self) -> Self {
|
||||||
self.add((
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
CONTENT_TYPE,
|
const HV_MIME: HeaderValue = HeaderValue::from_static("application/octet-stream");
|
||||||
HeaderValue::from_static("application/octet-stream"),
|
self.add((CONTENT_TYPE, HV_MIME))
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,27 +37,21 @@ type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::middleware::{ErrorHandlers, ErrorHandlerResponse};
|
/// use actix_web::http::{header, StatusCode};
|
||||||
/// use actix_web::{web, dev, App, HttpRequest, HttpResponse, Result};
|
/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
|
||||||
/// use actix_web::http::{StatusCode, header};
|
/// use actix_web::{dev, web, App, HttpResponse, Result};
|
||||||
///
|
|
||||||
/// fn render_500<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
|
||||||
/// res.response_mut()
|
|
||||||
/// .headers_mut()
|
|
||||||
/// .insert(header::CONTENT_TYPE, header::HeaderValue::from_static("Error"));
|
|
||||||
///
|
///
|
||||||
|
/// fn add_error_header<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||||
|
/// res.response_mut().headers_mut().insert(
|
||||||
|
/// header::CONTENT_TYPE,
|
||||||
|
/// header::HeaderValue::from_static("Error"),
|
||||||
|
/// );
|
||||||
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// let app = App::new()
|
/// let app = App::new()
|
||||||
/// .wrap(
|
/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header))
|
||||||
/// ErrorHandlers::new()
|
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
|
||||||
/// .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
|
|
||||||
/// )
|
|
||||||
/// .service(web::resource("/test")
|
|
||||||
/// .route(web::get().to(|| HttpResponse::Ok()))
|
|
||||||
/// .route(web::head().to(|| HttpResponse::MethodNotAllowed())
|
|
||||||
/// ));
|
|
||||||
/// ```
|
/// ```
|
||||||
pub struct ErrorHandlers<B> {
|
pub struct ErrorHandlers<B> {
|
||||||
handlers: Handlers<B>,
|
handlers: Handlers<B>,
|
||||||
|
|
|
@ -547,7 +547,7 @@ impl FormatText {
|
||||||
*self = FormatText::Str(s.to_string());
|
*self = FormatText::Str(s.to_string());
|
||||||
}
|
}
|
||||||
FormatText::RemoteAddr => {
|
FormatText::RemoteAddr => {
|
||||||
let s = if let Some(peer) = req.connection_info().remote_addr() {
|
let s = if let Some(peer) = req.connection_info().peer_addr() {
|
||||||
FormatText::Str((*peer).to_string())
|
FormatText::Str((*peer).to_string())
|
||||||
} else {
|
} else {
|
||||||
FormatText::Str("-".to_string())
|
FormatText::Str("-".to_string())
|
||||||
|
|
|
@ -5,6 +5,8 @@ mod condition;
|
||||||
mod default_headers;
|
mod default_headers;
|
||||||
mod err_handlers;
|
mod err_handlers;
|
||||||
mod logger;
|
mod logger;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod noop;
|
||||||
mod normalize;
|
mod normalize;
|
||||||
|
|
||||||
pub use self::compat::Compat;
|
pub use self::compat::Compat;
|
||||||
|
@ -12,6 +14,8 @@ pub use self::condition::Condition;
|
||||||
pub use self::default_headers::DefaultHeaders;
|
pub use self::default_headers::DefaultHeaders;
|
||||||
pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers};
|
pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers};
|
||||||
pub use self::logger::Logger;
|
pub use self::logger::Logger;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) use self::noop::Noop;
|
||||||
pub use self::normalize::{NormalizePath, TrailingSlash};
|
pub use self::normalize::{NormalizePath, TrailingSlash};
|
||||||
|
|
||||||
#[cfg(feature = "__compress")]
|
#[cfg(feature = "__compress")]
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
//! A no-op middleware. See [Noop] for docs.
|
||||||
|
|
||||||
|
use actix_utils::future::{ready, Ready};
|
||||||
|
|
||||||
|
use crate::dev::{Service, Transform};
|
||||||
|
|
||||||
|
/// A no-op middleware that passes through request and response untouched.
|
||||||
|
pub(crate) struct Noop;
|
||||||
|
|
||||||
|
impl<S: Service<Req>, Req> Transform<S, Req> for Noop {
|
||||||
|
type Response = S::Response;
|
||||||
|
type Error = S::Error;
|
||||||
|
type Transform = NoopService<S>;
|
||||||
|
type InitError = ();
|
||||||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
ready(Ok(NoopService { service }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(crate) struct NoopService<S> {
|
||||||
|
service: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Service<Req>, Req> Service<Req> for NoopService<S> {
|
||||||
|
type Response = S::Response;
|
||||||
|
type Error = S::Error;
|
||||||
|
type Future = S::Future;
|
||||||
|
|
||||||
|
crate::dev::forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, req: Req) -> Self::Future {
|
||||||
|
self.service.call(req)
|
||||||
|
}
|
||||||
|
}
|
|
@ -225,7 +225,7 @@ mod tests {
|
||||||
.service(web::resource("/v1/something").to(HttpResponse::Ok))
|
.service(web::resource("/v1/something").to(HttpResponse::Ok))
|
||||||
.service(
|
.service(
|
||||||
web::resource("/v2/something")
|
web::resource("/v2/something")
|
||||||
.guard(fn_guard(|req| req.uri.query() == Some("query=test")))
|
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
|
||||||
.to(HttpResponse::Ok),
|
.to(HttpResponse::Ok),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -261,7 +261,7 @@ mod tests {
|
||||||
.service(web::resource("/v1/something").to(HttpResponse::Ok))
|
.service(web::resource("/v1/something").to(HttpResponse::Ok))
|
||||||
.service(
|
.service(
|
||||||
web::resource("/v2/something")
|
web::resource("/v2/something")
|
||||||
.guard(fn_guard(|req| req.uri.query() == Some("query=test")))
|
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
|
||||||
.to(HttpResponse::Ok),
|
.to(HttpResponse::Ok),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -294,7 +294,7 @@ mod tests {
|
||||||
let app = init_service(
|
let app = init_service(
|
||||||
App::new().wrap(NormalizePath(TrailingSlash::Trim)).service(
|
App::new().wrap(NormalizePath(TrailingSlash::Trim)).service(
|
||||||
web::resource("/")
|
web::resource("/")
|
||||||
.guard(fn_guard(|req| req.uri.query() == Some("query=test")))
|
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
|
||||||
.to(HttpResponse::Ok),
|
.to(HttpResponse::Ok),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -318,7 +318,7 @@ mod tests {
|
||||||
.service(web::resource("/v1/something/").to(HttpResponse::Ok))
|
.service(web::resource("/v1/something/").to(HttpResponse::Ok))
|
||||||
.service(
|
.service(
|
||||||
web::resource("/v2/something/")
|
web::resource("/v2/something/")
|
||||||
.guard(fn_guard(|req| req.uri.query() == Some("query=test")))
|
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
|
||||||
.to(HttpResponse::Ok),
|
.to(HttpResponse::Ok),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -353,7 +353,7 @@ mod tests {
|
||||||
.wrap(NormalizePath(TrailingSlash::Always))
|
.wrap(NormalizePath(TrailingSlash::Always))
|
||||||
.service(
|
.service(
|
||||||
web::resource("/")
|
web::resource("/")
|
||||||
.guard(fn_guard(|req| req.uri.query() == Some("query=test")))
|
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
|
||||||
.to(HttpResponse::Ok),
|
.to(HttpResponse::Ok),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -378,7 +378,7 @@ mod tests {
|
||||||
.service(web::resource("/v1/").to(HttpResponse::Ok))
|
.service(web::resource("/v1/").to(HttpResponse::Ok))
|
||||||
.service(
|
.service(
|
||||||
web::resource("/v2/something")
|
web::resource("/v2/something")
|
||||||
.guard(fn_guard(|req| req.uri.query() == Some("query=test")))
|
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
|
||||||
.to(HttpResponse::Ok),
|
.to(HttpResponse::Ok),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -228,23 +228,28 @@ impl HttpRequest {
|
||||||
self.app_state().rmap()
|
self.app_state().rmap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peer socket address.
|
/// Returns peer socket address.
|
||||||
///
|
///
|
||||||
/// Peer address is the directly connected peer's socket address. If a proxy is used in front of
|
/// Peer address is the directly connected peer's socket address. If a proxy is used in front of
|
||||||
/// the Actix Web server, then it would be address of this proxy.
|
/// the Actix Web server, then it would be address of this proxy.
|
||||||
///
|
///
|
||||||
/// To get client connection information `.connection_info()` should be used.
|
/// For expanded client connection information, use [`connection_info`] instead.
|
||||||
///
|
///
|
||||||
/// Will only return None when called in unit tests.
|
/// Will only return None when called in unit tests unless [`TestRequest::peer_addr`] is used.
|
||||||
|
///
|
||||||
|
/// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr
|
||||||
|
/// [`connection_info`]: Self::connection_info
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn peer_addr(&self) -> Option<net::SocketAddr> {
|
pub fn peer_addr(&self) -> Option<net::SocketAddr> {
|
||||||
self.head().peer_addr
|
self.head().peer_addr
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get *ConnectionInfo* for the current request.
|
/// Returns connection info for the current request.
|
||||||
///
|
///
|
||||||
/// This method panics if request's extensions container is already
|
/// The return type, [`ConnectionInfo`], can also be used as an extractor.
|
||||||
/// borrowed.
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if request's extensions container is already borrowed.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
|
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
|
||||||
if !self.extensions().contains::<ConnectionInfo>() {
|
if !self.extensions().contains::<ConnectionInfo>() {
|
||||||
|
@ -252,7 +257,7 @@ impl HttpRequest {
|
||||||
self.extensions_mut().insert(info);
|
self.extensions_mut().insert(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ref::map(self.extensions(), |e| e.get().unwrap())
|
Ref::map(self.extensions(), |data| data.get().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// App config
|
/// App config
|
||||||
|
|
128
src/resource.rs
128
src/resource.rs
|
@ -1,6 +1,6 @@
|
||||||
use std::{cell::RefCell, fmt, future::Future, rc::Rc};
|
use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc};
|
||||||
|
|
||||||
use actix_http::Extensions;
|
use actix_http::{body::BoxBody, Extensions};
|
||||||
use actix_router::{IntoPatterns, Patterns};
|
use actix_router::{IntoPatterns, Patterns};
|
||||||
use actix_service::{
|
use actix_service::{
|
||||||
apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory,
|
apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory,
|
||||||
|
@ -20,32 +20,29 @@ use crate::{
|
||||||
BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
|
BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
|
||||||
ServiceResponse,
|
ServiceResponse,
|
||||||
},
|
},
|
||||||
BoxError, Error, FromRequest, HttpResponse, Responder,
|
Error, FromRequest, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// *Resource* is an entry in resources table which corresponds to requested URL.
|
/// A collection of [`Route`]s that respond to the same path pattern.
|
||||||
///
|
///
|
||||||
/// Resource in turn has at least one route.
|
/// Resource in turn has at least one route. Route consists of an handlers objects and list of
|
||||||
/// Route consists of an handlers objects and list of guards
|
/// guards (objects that implement `Guard` trait). Resources and routes uses builder-like pattern
|
||||||
/// (objects that implement `Guard` trait).
|
/// for configuration. During request handling, resource object iterate through all routes and check
|
||||||
/// Resources and routes uses builder-like pattern for configuration.
|
/// guards for specific route, if request matches all guards, route considered matched and route
|
||||||
/// During request handling, resource object iterate through all routes
|
/// handler get called.
|
||||||
/// and check guards for specific route, if request matches all
|
|
||||||
/// guards, route considered matched and route handler get called.
|
|
||||||
///
|
///
|
||||||
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, App, HttpResponse};
|
/// use actix_web::{web, App, HttpResponse};
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new().service(
|
||||||
/// let app = App::new().service(
|
/// web::resource("/")
|
||||||
/// web::resource("/")
|
/// .route(web::get().to(|| HttpResponse::Ok())));
|
||||||
/// .route(web::get().to(|| HttpResponse::Ok())));
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// If no matching route could be found, *405* response code get returned.
|
/// If no matching route could be found, *405* response code get returned. Default behavior could be
|
||||||
/// Default behavior could be overridden with `default_resource()` method.
|
/// overridden with `default_resource()` method.
|
||||||
pub struct Resource<T = ResourceEndpoint> {
|
pub struct Resource<T = ResourceEndpoint, B = BoxBody> {
|
||||||
endpoint: T,
|
endpoint: T,
|
||||||
rdef: Patterns,
|
rdef: Patterns,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
@ -54,6 +51,7 @@ pub struct Resource<T = ResourceEndpoint> {
|
||||||
guards: Vec<Box<dyn Guard>>,
|
guards: Vec<Box<dyn Guard>>,
|
||||||
default: BoxedHttpServiceFactory,
|
default: BoxedHttpServiceFactory,
|
||||||
factory_ref: Rc<RefCell<Option<ResourceFactory>>>,
|
factory_ref: Rc<RefCell<Option<ResourceFactory>>>,
|
||||||
|
_phantom: PhantomData<B>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resource {
|
impl Resource {
|
||||||
|
@ -71,19 +69,21 @@ impl Resource {
|
||||||
default: boxed::factory(fn_service(|req: ServiceRequest| async {
|
default: boxed::factory(fn_service(|req: ServiceRequest| async {
|
||||||
Ok(req.into_response(HttpResponse::MethodNotAllowed()))
|
Ok(req.into_response(HttpResponse::MethodNotAllowed()))
|
||||||
})),
|
})),
|
||||||
|
_phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Resource<T>
|
impl<T, B> Resource<T, B>
|
||||||
where
|
where
|
||||||
T: ServiceFactory<
|
T: ServiceFactory<
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
Config = (),
|
Config = (),
|
||||||
Response = ServiceResponse,
|
Response = ServiceResponse<B>,
|
||||||
Error = Error,
|
Error = Error,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
>,
|
>,
|
||||||
|
B: MessageBody,
|
||||||
{
|
{
|
||||||
/// Set resource name.
|
/// Set resource name.
|
||||||
///
|
///
|
||||||
|
@ -131,15 +131,13 @@ where
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, guard, App, HttpResponse};
|
/// use actix_web::{web, guard, App, HttpResponse};
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new().service(
|
||||||
/// let app = App::new().service(
|
/// web::resource("/").route(
|
||||||
/// web::resource("/").route(
|
/// web::route()
|
||||||
/// web::route()
|
/// .guard(guard::Any(guard::Get()).or(guard::Put()))
|
||||||
/// .guard(guard::Any(guard::Get()).or(guard::Put()))
|
/// .guard(guard::Header("Content-Type", "text/plain"))
|
||||||
/// .guard(guard::Header("Content-Type", "text/plain"))
|
/// .to(|| HttpResponse::Ok()))
|
||||||
/// .to(|| HttpResponse::Ok()))
|
/// );
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Multiple routes could be added to a resource. Resource object uses
|
/// Multiple routes could be added to a resource. Resource object uses
|
||||||
|
@ -232,14 +230,11 @@ where
|
||||||
/// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() }
|
/// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() }
|
||||||
/// App::new().service(web::resource("/").route(web::route().to(index)));
|
/// App::new().service(web::resource("/").route(web::route().to(index)));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn to<F, I, R>(mut self, handler: F) -> Self
|
pub fn to<F, Args>(mut self, handler: F) -> Self
|
||||||
where
|
where
|
||||||
F: Handler<I, R>,
|
F: Handler<Args>,
|
||||||
I: FromRequest + 'static,
|
Args: FromRequest + 'static,
|
||||||
R: Future + 'static,
|
F::Output: Responder + 'static,
|
||||||
R::Output: Responder + 'static,
|
|
||||||
<R::Output as Responder>::Body: MessageBody,
|
|
||||||
<<R::Output as Responder>::Body as MessageBody>::Error: Into<BoxError>,
|
|
||||||
{
|
{
|
||||||
self.routes.push(Route::new().to(handler));
|
self.routes.push(Route::new().to(handler));
|
||||||
self
|
self
|
||||||
|
@ -252,26 +247,28 @@ where
|
||||||
/// type (i.e modify response's body).
|
/// type (i.e modify response's body).
|
||||||
///
|
///
|
||||||
/// **Note**: middlewares get called in opposite order of middlewares registration.
|
/// **Note**: middlewares get called in opposite order of middlewares registration.
|
||||||
pub fn wrap<M>(
|
pub fn wrap<M, B1>(
|
||||||
self,
|
self,
|
||||||
mw: M,
|
mw: M,
|
||||||
) -> Resource<
|
) -> Resource<
|
||||||
impl ServiceFactory<
|
impl ServiceFactory<
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
Config = (),
|
Config = (),
|
||||||
Response = ServiceResponse,
|
Response = ServiceResponse<B1>,
|
||||||
Error = Error,
|
Error = Error,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
>,
|
>,
|
||||||
|
B1,
|
||||||
>
|
>
|
||||||
where
|
where
|
||||||
M: Transform<
|
M: Transform<
|
||||||
T::Service,
|
T::Service,
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
Response = ServiceResponse,
|
Response = ServiceResponse<B1>,
|
||||||
Error = Error,
|
Error = Error,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
>,
|
>,
|
||||||
|
B1: MessageBody,
|
||||||
{
|
{
|
||||||
Resource {
|
Resource {
|
||||||
endpoint: apply(mw, self.endpoint),
|
endpoint: apply(mw, self.endpoint),
|
||||||
|
@ -282,6 +279,7 @@ where
|
||||||
default: self.default,
|
default: self.default,
|
||||||
app_data: self.app_data,
|
app_data: self.app_data,
|
||||||
factory_ref: self.factory_ref,
|
factory_ref: self.factory_ref,
|
||||||
|
_phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,21 +317,23 @@ where
|
||||||
/// .route(web::get().to(index)));
|
/// .route(web::get().to(index)));
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn wrap_fn<F, R>(
|
pub fn wrap_fn<F, R, B1>(
|
||||||
self,
|
self,
|
||||||
mw: F,
|
mw: F,
|
||||||
) -> Resource<
|
) -> Resource<
|
||||||
impl ServiceFactory<
|
impl ServiceFactory<
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
Config = (),
|
Config = (),
|
||||||
Response = ServiceResponse,
|
Response = ServiceResponse<B1>,
|
||||||
Error = Error,
|
Error = Error,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
>,
|
>,
|
||||||
|
B1,
|
||||||
>
|
>
|
||||||
where
|
where
|
||||||
F: Fn(ServiceRequest, &T::Service) -> R + Clone,
|
F: Fn(ServiceRequest, &T::Service) -> R + Clone,
|
||||||
R: Future<Output = Result<ServiceResponse, Error>>,
|
R: Future<Output = Result<ServiceResponse<B1>, Error>>,
|
||||||
|
B1: MessageBody,
|
||||||
{
|
{
|
||||||
Resource {
|
Resource {
|
||||||
endpoint: apply_fn_factory(self.endpoint, mw),
|
endpoint: apply_fn_factory(self.endpoint, mw),
|
||||||
|
@ -344,6 +344,7 @@ where
|
||||||
default: self.default,
|
default: self.default,
|
||||||
app_data: self.app_data,
|
app_data: self.app_data,
|
||||||
factory_ref: self.factory_ref,
|
factory_ref: self.factory_ref,
|
||||||
|
_phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,15 +372,16 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> HttpServiceFactory for Resource<T>
|
impl<T, B> HttpServiceFactory for Resource<T, B>
|
||||||
where
|
where
|
||||||
T: ServiceFactory<
|
T: ServiceFactory<
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
Config = (),
|
Config = (),
|
||||||
Response = ServiceResponse,
|
Response = ServiceResponse<B>,
|
||||||
Error = Error,
|
Error = Error,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
> + 'static,
|
> + 'static,
|
||||||
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
fn register(mut self, config: &mut AppService) {
|
fn register(mut self, config: &mut AppService) {
|
||||||
let guards = if self.guards.is_empty() {
|
let guards = if self.guards.is_empty() {
|
||||||
|
@ -411,7 +413,9 @@ where
|
||||||
req.add_data_container(Rc::clone(data));
|
req.add_data_container(Rc::clone(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.call(req)
|
let fut = srv.call(req);
|
||||||
|
|
||||||
|
async { Ok(fut.await?.map_into_boxed_body()) }
|
||||||
});
|
});
|
||||||
|
|
||||||
config.register_service(rdef, guards, endpoint, None)
|
config.register_service(rdef, guards, endpoint, None)
|
||||||
|
@ -534,11 +538,11 @@ mod tests {
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
web::resource("/test-compat")
|
web::resource("/test-compat")
|
||||||
// .wrap_fn(|req, srv| {
|
.wrap_fn(|req, srv| {
|
||||||
// let fut = srv.call(req);
|
let fut = srv.call(req);
|
||||||
// async { Ok(fut.await?.map_into_right_body::<()>()) }
|
async { Ok(fut.await?.map_into_right_body::<()>()) }
|
||||||
// })
|
})
|
||||||
.wrap(Compat::new(DefaultHeaders::new()))
|
.wrap(Compat::noop())
|
||||||
.route(web::get().to(|| async { "hello" }))
|
.route(web::get().to(|| async { "hello" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -801,4 +805,26 @@ mod tests {
|
||||||
let resp = call_service(&srv, req).await;
|
let resp = call_service(&srv, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_middleware_body_type() {
|
||||||
|
let srv = init_service(
|
||||||
|
App::new().service(
|
||||||
|
web::resource("/test")
|
||||||
|
.wrap_fn(|req, srv| {
|
||||||
|
let fut = srv.call(req);
|
||||||
|
async { Ok(fut.await?.map_into_right_body::<()>()) }
|
||||||
|
})
|
||||||
|
.route(web::get().to(|| async { "hello" })),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// test if `try_into_bytes()` is preserved across scope layer
|
||||||
|
use actix_http::body::MessageBody as _;
|
||||||
|
let req = TestRequest::with_uri("/test").to_request();
|
||||||
|
let resp = call_service(&srv, req).await;
|
||||||
|
let body = resp.into_body();
|
||||||
|
assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -429,9 +429,12 @@ mod tests {
|
||||||
use actix_http::body;
|
use actix_http::body;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::http::{
|
use crate::{
|
||||||
header::{self, HeaderValue, CONTENT_TYPE},
|
http::{
|
||||||
StatusCode,
|
header::{self, HeaderValue, CONTENT_TYPE},
|
||||||
|
StatusCode,
|
||||||
|
},
|
||||||
|
test::assert_body_eq,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -472,32 +475,23 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_json() {
|
async fn test_json() {
|
||||||
let resp = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]);
|
let res = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]);
|
||||||
let ct = resp.headers().get(CONTENT_TYPE).unwrap();
|
let ct = res.headers().get(CONTENT_TYPE).unwrap();
|
||||||
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
||||||
assert_eq!(
|
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
||||||
body::to_bytes(resp.into_body()).await.unwrap().as_ref(),
|
|
||||||
br#"["v1","v2","v3"]"#
|
|
||||||
);
|
|
||||||
|
|
||||||
let resp = HttpResponse::Ok().json(&["v1", "v2", "v3"]);
|
let res = HttpResponse::Ok().json(&["v1", "v2", "v3"]);
|
||||||
let ct = resp.headers().get(CONTENT_TYPE).unwrap();
|
let ct = res.headers().get(CONTENT_TYPE).unwrap();
|
||||||
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
||||||
assert_eq!(
|
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
||||||
body::to_bytes(resp.into_body()).await.unwrap().as_ref(),
|
|
||||||
br#"["v1","v2","v3"]"#
|
|
||||||
);
|
|
||||||
|
|
||||||
// content type override
|
// content type override
|
||||||
let resp = HttpResponse::Ok()
|
let res = HttpResponse::Ok()
|
||||||
.insert_header((CONTENT_TYPE, "text/json"))
|
.insert_header((CONTENT_TYPE, "text/json"))
|
||||||
.json(&vec!["v1", "v2", "v3"]);
|
.json(&vec!["v1", "v2", "v3"]);
|
||||||
let ct = resp.headers().get(CONTENT_TYPE).unwrap();
|
let ct = res.headers().get(CONTENT_TYPE).unwrap();
|
||||||
assert_eq!(ct, HeaderValue::from_static("text/json"));
|
assert_eq!(ct, HeaderValue::from_static("text/json"));
|
||||||
assert_eq!(
|
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
||||||
body::to_bytes(resp.into_body()).await.unwrap().as_ref(),
|
|
||||||
br#"["v1","v2","v3"]"#
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
|
60
src/route.rs
60
src/route.rs
|
@ -1,4 +1,4 @@
|
||||||
use std::{future::Future, mem, rc::Rc};
|
use std::{mem, rc::Rc};
|
||||||
|
|
||||||
use actix_http::Method;
|
use actix_http::Method;
|
||||||
use actix_service::{
|
use actix_service::{
|
||||||
|
@ -8,17 +8,16 @@ use actix_service::{
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::MessageBody,
|
|
||||||
guard::{self, Guard},
|
guard::{self, Guard},
|
||||||
handler::{handler_service, Handler},
|
handler::{handler_service, Handler},
|
||||||
service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse},
|
service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse},
|
||||||
BoxError, Error, FromRequest, HttpResponse, Responder,
|
Error, FromRequest, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Resource route definition
|
/// A request handler with [guards](guard).
|
||||||
///
|
///
|
||||||
/// Route uses builder-like pattern for configuration.
|
/// Route uses a builder-like pattern for configuration. If handler is not set, a `404 Not Found`
|
||||||
/// If handler is not explicitly set, default *404 Not Found* handler is used.
|
/// handler is used.
|
||||||
pub struct Route {
|
pub struct Route {
|
||||||
service: BoxedHttpServiceFactory,
|
service: BoxedHttpServiceFactory,
|
||||||
guards: Rc<Vec<Box<dyn Guard>>>,
|
guards: Rc<Vec<Box<dyn Guard>>>,
|
||||||
|
@ -66,9 +65,12 @@ pub struct RouteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RouteService {
|
impl RouteService {
|
||||||
|
// TODO: does this need to take &mut ?
|
||||||
pub fn check(&self, req: &mut ServiceRequest) -> bool {
|
pub fn check(&self, req: &mut ServiceRequest) -> bool {
|
||||||
for f in self.guards.iter() {
|
let guard_ctx = req.guard_ctx();
|
||||||
if !f.check(req.head()) {
|
|
||||||
|
for guard in self.guards.iter() {
|
||||||
|
if !guard.check(&guard_ctx) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,6 +93,7 @@ impl Service<ServiceRequest> for RouteService {
|
||||||
impl Route {
|
impl Route {
|
||||||
/// Add method guard to the route.
|
/// Add method guard to the route.
|
||||||
///
|
///
|
||||||
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_web::*;
|
/// # use actix_web::*;
|
||||||
/// # fn main() {
|
/// # fn main() {
|
||||||
|
@ -111,6 +114,7 @@ impl Route {
|
||||||
|
|
||||||
/// Add guard to the route.
|
/// Add guard to the route.
|
||||||
///
|
///
|
||||||
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_web::*;
|
/// # use actix_web::*;
|
||||||
/// # fn main() {
|
/// # fn main() {
|
||||||
|
@ -144,16 +148,13 @@ impl Route {
|
||||||
/// format!("Welcome {}!", info.username)
|
/// format!("Welcome {}!", info.username)
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new().service(
|
||||||
/// let app = App::new().service(
|
/// web::resource("/{username}/index.html") // <- define path parameters
|
||||||
/// web::resource("/{username}/index.html") // <- define path parameters
|
/// .route(web::get().to(index)) // <- register handler
|
||||||
/// .route(web::get().to(index)) // <- register handler
|
/// );
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// It is possible to use multiple extractors for one handler function.
|
/// It is possible to use multiple extractors for one handler function.
|
||||||
///
|
|
||||||
/// ```
|
/// ```
|
||||||
/// # use std::collections::HashMap;
|
/// # use std::collections::HashMap;
|
||||||
/// # use serde::Deserialize;
|
/// # use serde::Deserialize;
|
||||||
|
@ -165,25 +166,24 @@ impl Route {
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// /// extract path info using serde
|
/// /// extract path info using serde
|
||||||
/// async fn index(path: web::Path<Info>, query: web::Query<HashMap<String, String>>, body: web::Json<Info>) -> String {
|
/// async fn index(
|
||||||
|
/// path: web::Path<Info>,
|
||||||
|
/// query: web::Query<HashMap<String, String>>,
|
||||||
|
/// body: web::Json<Info>
|
||||||
|
/// ) -> String {
|
||||||
/// format!("Welcome {}!", path.username)
|
/// format!("Welcome {}!", path.username)
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new().service(
|
||||||
/// let app = App::new().service(
|
/// web::resource("/{username}/index.html") // <- define path parameters
|
||||||
/// web::resource("/{username}/index.html") // <- define path parameters
|
/// .route(web::get().to(index))
|
||||||
/// .route(web::get().to(index))
|
/// );
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn to<F, T, R>(mut self, handler: F) -> Self
|
pub fn to<F, Args>(mut self, handler: F) -> Self
|
||||||
where
|
where
|
||||||
F: Handler<T, R>,
|
F: Handler<Args>,
|
||||||
T: FromRequest + 'static,
|
Args: FromRequest + 'static,
|
||||||
R: Future + 'static,
|
F::Output: Responder + 'static,
|
||||||
R::Output: Responder + 'static,
|
|
||||||
<R::Output as Responder>::Body: MessageBody,
|
|
||||||
<<R::Output as Responder>::Body as MessageBody>::Error: Into<BoxError>,
|
|
||||||
{
|
{
|
||||||
self.service = handler_service(handler);
|
self.service = handler_service(handler);
|
||||||
self
|
self
|
||||||
|
@ -203,7 +203,7 @@ impl Route {
|
||||||
/// type Error = Infallible;
|
/// type Error = Infallible;
|
||||||
/// type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
/// type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
///
|
///
|
||||||
/// always_ready!();
|
/// dev::always_ready!();
|
||||||
///
|
///
|
||||||
/// fn call(&self, req: ServiceRequest) -> Self::Future {
|
/// fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
/// let (req, _) = req.into_parts();
|
/// let (req, _) = req.into_parts();
|
||||||
|
|
181
src/scope.rs
181
src/scope.rs
|
@ -1,6 +1,9 @@
|
||||||
use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, mem, rc::Rc};
|
use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, mem, rc::Rc};
|
||||||
|
|
||||||
use actix_http::{body::BoxBody, Extensions};
|
use actix_http::{
|
||||||
|
body::{BoxBody, MessageBody},
|
||||||
|
Extensions,
|
||||||
|
};
|
||||||
use actix_router::{ResourceDef, Router};
|
use actix_router::{ResourceDef, Router};
|
||||||
use actix_service::{
|
use actix_service::{
|
||||||
apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory,
|
apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory,
|
||||||
|
@ -24,34 +27,36 @@ use crate::{
|
||||||
|
|
||||||
type Guards = Vec<Box<dyn Guard>>;
|
type Guards = Vec<Box<dyn Guard>>;
|
||||||
|
|
||||||
/// Resources scope.
|
/// A collection of [`Route`]s, [`Resource`]s, or other services that share a common path prefix.
|
||||||
///
|
///
|
||||||
/// Scope is a set of resources with common root path.
|
/// The `Scope`'s path can contain [dynamic segments]. The dynamic segments can be extracted from
|
||||||
/// Scopes collect multiple paths under a common path prefix.
|
/// requests using the [`Path`](crate::web::Path) extractor or
|
||||||
/// Scope path can contain variable path segments as resources.
|
/// with [`HttpRequest::match_info()`](crate::HttpRequest::match_info).
|
||||||
/// Scope prefix is always complete path segment, i.e `/app` would
|
|
||||||
/// be converted to a `/app/` and it would not match `/app` path.
|
|
||||||
///
|
///
|
||||||
/// You can get variable path segments from `HttpRequest::match_info()`.
|
/// # Avoid Trailing Slashes
|
||||||
/// `Path` extractor also is able to extract scope level variable segments.
|
/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost
|
||||||
|
/// certainly not have the expected behavior. See the [documentation on resource definitions][pat]
|
||||||
|
/// to understand why this is the case and how to correctly construct scope/prefix definitions.
|
||||||
///
|
///
|
||||||
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, App, HttpResponse};
|
/// use actix_web::{web, App, HttpResponse};
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new().service(
|
||||||
/// let app = App::new().service(
|
/// web::scope("/{project_id}/")
|
||||||
/// web::scope("/{project_id}/")
|
/// .service(web::resource("/path1").to(|| async { "OK" }))
|
||||||
/// .service(web::resource("/path1").to(|| async { "OK" }))
|
/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok())))
|
||||||
/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok())))
|
/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed)))
|
||||||
/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed)))
|
/// );
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// In the above example three routes get registered:
|
/// In the above example three routes get registered:
|
||||||
/// * /{project_id}/path1 - responds to all http method
|
/// - /{project_id}/path1 - responds to all HTTP methods
|
||||||
/// * /{project_id}/path2 - `GET` requests
|
/// - /{project_id}/path2 - responds to `GET` requests
|
||||||
/// * /{project_id}/path3 - `HEAD` requests
|
/// - /{project_id}/path3 - responds to `HEAD` requests
|
||||||
|
///
|
||||||
|
/// [pat]: crate::dev::ResourceDef#prefix-resources
|
||||||
|
/// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments
|
||||||
pub struct Scope<T = ScopeEndpoint, B = BoxBody> {
|
pub struct Scope<T = ScopeEndpoint, B = BoxBody> {
|
||||||
endpoint: T,
|
endpoint: T,
|
||||||
rdef: String,
|
rdef: String,
|
||||||
|
@ -103,16 +108,14 @@ where
|
||||||
/// "Welcome!"
|
/// "Welcome!"
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new().service(
|
||||||
/// let app = App::new().service(
|
/// web::scope("/app")
|
||||||
/// web::scope("/app")
|
/// .guard(guard::Header("content-type", "text/plain"))
|
||||||
/// .guard(guard::Header("content-type", "text/plain"))
|
/// .route("/test1", web::get().to(index))
|
||||||
/// .route("/test1", web::get().to(index))
|
/// .route("/test2", web::post().to(|r: HttpRequest| {
|
||||||
/// .route("/test2", web::post().to(|r: HttpRequest| {
|
/// HttpResponse::MethodNotAllowed()
|
||||||
/// HttpResponse::MethodNotAllowed()
|
/// }))
|
||||||
/// }))
|
/// );
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
|
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
|
||||||
self.guards.push(Box::new(guard));
|
self.guards.push(Box::new(guard));
|
||||||
|
@ -183,15 +186,13 @@ where
|
||||||
/// );
|
/// );
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new()
|
||||||
/// let app = App::new()
|
/// .wrap(middleware::Logger::default())
|
||||||
/// .wrap(middleware::Logger::default())
|
/// .service(
|
||||||
/// .service(
|
/// web::scope("/api")
|
||||||
/// web::scope("/api")
|
/// .configure(config)
|
||||||
/// .configure(config)
|
/// )
|
||||||
/// )
|
/// .route("/index.html", web::get().to(|| HttpResponse::Ok()));
|
||||||
/// .route("/index.html", web::get().to(|| HttpResponse::Ok()));
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn configure<F>(mut self, cfg_fn: F) -> Self
|
pub fn configure<F>(mut self, cfg_fn: F) -> Self
|
||||||
where
|
where
|
||||||
|
@ -230,13 +231,11 @@ where
|
||||||
/// "Welcome!"
|
/// "Welcome!"
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new().service(
|
||||||
/// let app = App::new().service(
|
/// web::scope("/app").service(
|
||||||
/// web::scope("/app").service(
|
/// web::scope("/v1")
|
||||||
/// web::scope("/v1")
|
/// .service(web::resource("/test1").to(index)))
|
||||||
/// .service(web::resource("/test1").to(index)))
|
/// );
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn service<F>(mut self, factory: F) -> Self
|
pub fn service<F>(mut self, factory: F) -> Self
|
||||||
where
|
where
|
||||||
|
@ -260,13 +259,11 @@ where
|
||||||
/// "Welcome!"
|
/// "Welcome!"
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new().service(
|
||||||
/// let app = App::new().service(
|
/// web::scope("/app")
|
||||||
/// web::scope("/app")
|
/// .route("/test1", web::get().to(index))
|
||||||
/// .route("/test1", web::get().to(index))
|
/// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed()))
|
||||||
/// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed()))
|
/// );
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn route(self, path: &str, mut route: Route) -> Self {
|
pub fn route(self, path: &str, mut route: Route) -> Self {
|
||||||
self.service(
|
self.service(
|
||||||
|
@ -352,21 +349,19 @@ where
|
||||||
/// "Welcome!"
|
/// "Welcome!"
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// let app = App::new().service(
|
||||||
/// let app = App::new().service(
|
/// web::scope("/app")
|
||||||
/// web::scope("/app")
|
/// .wrap_fn(|req, srv| {
|
||||||
/// .wrap_fn(|req, srv| {
|
/// let fut = srv.call(req);
|
||||||
/// let fut = srv.call(req);
|
/// async {
|
||||||
/// async {
|
/// let mut res = fut.await?;
|
||||||
/// let mut res = fut.await?;
|
/// res.headers_mut().insert(
|
||||||
/// res.headers_mut().insert(
|
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"),
|
||||||
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"),
|
/// );
|
||||||
/// );
|
/// Ok(res)
|
||||||
/// Ok(res)
|
/// }
|
||||||
/// }
|
/// })
|
||||||
/// })
|
/// .route("/index.html", web::get().to(index)));
|
||||||
/// .route("/index.html", web::get().to(index)));
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn wrap_fn<F, R, B1>(
|
pub fn wrap_fn<F, R, B1>(
|
||||||
self,
|
self,
|
||||||
|
@ -399,15 +394,16 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> HttpServiceFactory for Scope<T>
|
impl<T, B> HttpServiceFactory for Scope<T, B>
|
||||||
where
|
where
|
||||||
T: ServiceFactory<
|
T: ServiceFactory<
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
Config = (),
|
Config = (),
|
||||||
Response = ServiceResponse,
|
Response = ServiceResponse<B>,
|
||||||
Error = Error,
|
Error = Error,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
> + 'static,
|
> + 'static,
|
||||||
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
fn register(mut self, config: &mut AppService) {
|
fn register(mut self, config: &mut AppService) {
|
||||||
// update default resource if needed
|
// update default resource if needed
|
||||||
|
@ -457,7 +453,9 @@ where
|
||||||
req.add_data_container(Rc::clone(data));
|
req.add_data_container(Rc::clone(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.call(req)
|
let fut = srv.call(req);
|
||||||
|
|
||||||
|
async { Ok(fut.await?.map_into_boxed_body()) }
|
||||||
});
|
});
|
||||||
|
|
||||||
// register final service
|
// register final service
|
||||||
|
@ -471,6 +469,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ScopeFactory {
|
pub struct ScopeFactory {
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
services: Rc<
|
services: Rc<
|
||||||
[(
|
[(
|
||||||
ResourceDef,
|
ResourceDef,
|
||||||
|
@ -539,12 +538,15 @@ impl Service<ServiceRequest> for ScopeService {
|
||||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||||
let res = self.router.recognize_fn(&mut req, |req, guards| {
|
let res = self.router.recognize_fn(&mut req, |req, guards| {
|
||||||
if let Some(ref guards) = guards {
|
if let Some(ref guards) = guards {
|
||||||
for f in guards {
|
let guard_ctx = req.guard_ctx();
|
||||||
if !f.check(req.head()) {
|
|
||||||
|
for guard in guards {
|
||||||
|
if !guard.check(&guard_ctx) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -616,11 +618,11 @@ mod tests {
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
web::scope("/test-compat")
|
web::scope("/test-compat")
|
||||||
// .wrap_fn(|req, srv| {
|
.wrap_fn(|req, srv| {
|
||||||
// let fut = srv.call(req);
|
let fut = srv.call(req);
|
||||||
// async { Ok(fut.await?.map_into_right_body::<()>()) }
|
async { Ok(fut.await?.map_into_right_body::<()>()) }
|
||||||
// })
|
})
|
||||||
.wrap(Compat::new(DefaultHeaders::new()))
|
.wrap(Compat::noop())
|
||||||
.service(web::resource("").route(web::get().to(|| async { "hello" })))
|
.service(web::resource("").route(web::get().to(|| async { "hello" })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -980,6 +982,29 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_middleware_body_type() {
|
||||||
|
// Compile test that Scope accepts any body type; test for `EitherBody`
|
||||||
|
let srv = init_service(
|
||||||
|
App::new().service(
|
||||||
|
web::scope("app")
|
||||||
|
.wrap_fn(|req, srv| {
|
||||||
|
let fut = srv.call(req);
|
||||||
|
async { Ok(fut.await?.map_into_right_body::<()>()) }
|
||||||
|
})
|
||||||
|
.service(web::resource("/test").route(web::get().to(|| async { "hello" }))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// test if `MessageBody::try_into_bytes()` is preserved across scope layer
|
||||||
|
use actix_http::body::MessageBody as _;
|
||||||
|
let req = TestRequest::with_uri("/app/test").to_request();
|
||||||
|
let resp = call_service(&srv, req).await;
|
||||||
|
let body = resp.into_body();
|
||||||
|
assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_middleware_fn() {
|
async fn test_middleware_fn() {
|
||||||
let srv = init_service(
|
let srv = init_service(
|
||||||
|
|
|
@ -63,6 +63,7 @@ where
|
||||||
backlog: u32,
|
backlog: u32,
|
||||||
sockets: Vec<Socket>,
|
sockets: Vec<Socket>,
|
||||||
builder: ServerBuilder,
|
builder: ServerBuilder,
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
on_connect_fn: Option<Arc<dyn Fn(&dyn Any, &mut Extensions) + Send + Sync>>,
|
on_connect_fn: Option<Arc<dyn Fn(&dyn Any, &mut Extensions) + Send + Sync>>,
|
||||||
_phantom: PhantomData<(S, B)>,
|
_phantom: PhantomData<(S, B)>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
body::{BoxBody, EitherBody, MessageBody},
|
body::{BoxBody, EitherBody, MessageBody},
|
||||||
header::HeaderMap,
|
header::HeaderMap,
|
||||||
Extensions, HttpMessage, Method, Payload, PayloadStream, RequestHead, Response,
|
BoxedPayloadStream, Extensions, HttpMessage, Method, Payload, RequestHead, Response,
|
||||||
ResponseHead, StatusCode, Uri, Version,
|
ResponseHead, StatusCode, Uri, Version,
|
||||||
};
|
};
|
||||||
use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url};
|
use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url};
|
||||||
|
@ -21,7 +21,7 @@ use cookie::{Cookie, ParseError as CookieParseError};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{AppConfig, AppService},
|
config::{AppConfig, AppService},
|
||||||
dev::ensure_leading_slash,
|
dev::ensure_leading_slash,
|
||||||
guard::Guard,
|
guard::{Guard, GuardContext},
|
||||||
info::ConnectionInfo,
|
info::ConnectionInfo,
|
||||||
rmap::ResourceMap,
|
rmap::ResourceMap,
|
||||||
Error, HttpRequest, HttpResponse,
|
Error, HttpRequest, HttpResponse,
|
||||||
|
@ -172,7 +172,7 @@ impl ServiceRequest {
|
||||||
self.head().uri.path()
|
self.head().uri.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Counterpart to [`HttpRequest::query_string`](super::HttpRequest::query_string()).
|
/// Counterpart to [`HttpRequest::query_string`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn query_string(&self) -> &str {
|
pub fn query_string(&self) -> &str {
|
||||||
self.req.query_string()
|
self.req.query_string()
|
||||||
|
@ -208,13 +208,13 @@ impl ServiceRequest {
|
||||||
self.req.match_info()
|
self.req.match_info()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Counterpart to [`HttpRequest::match_name`](super::HttpRequest::match_name()).
|
/// Counterpart to [`HttpRequest::match_name`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn match_name(&self) -> Option<&str> {
|
pub fn match_name(&self) -> Option<&str> {
|
||||||
self.req.match_name()
|
self.req.match_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Counterpart to [`HttpRequest::match_pattern`](super::HttpRequest::match_pattern()).
|
/// Counterpart to [`HttpRequest::match_pattern`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn match_pattern(&self) -> Option<String> {
|
pub fn match_pattern(&self) -> Option<String> {
|
||||||
self.req.match_pattern()
|
self.req.match_pattern()
|
||||||
|
@ -238,7 +238,7 @@ impl ServiceRequest {
|
||||||
self.req.app_config()
|
self.req.app_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Counterpart to [`HttpRequest::app_data`](super::HttpRequest::app_data()).
|
/// Counterpart to [`HttpRequest::app_data`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn app_data<T: 'static>(&self) -> Option<&T> {
|
pub fn app_data<T: 'static>(&self) -> Option<&T> {
|
||||||
for container in self.req.inner.app_data.iter().rev() {
|
for container in self.req.inner.app_data.iter().rev() {
|
||||||
|
@ -250,19 +250,33 @@ impl ServiceRequest {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Counterpart to [`HttpRequest::conn_data`](super::HttpRequest::conn_data()).
|
/// Counterpart to [`HttpRequest::conn_data`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn conn_data<T: 'static>(&self) -> Option<&T> {
|
pub fn conn_data<T: 'static>(&self) -> Option<&T> {
|
||||||
self.req.conn_data()
|
self.req.conn_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Counterpart to [`HttpRequest::req_data`].
|
||||||
|
#[inline]
|
||||||
|
pub fn req_data(&self) -> Ref<'_, Extensions> {
|
||||||
|
self.req.req_data()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Counterpart to [`HttpRequest::req_data_mut`].
|
||||||
|
#[inline]
|
||||||
|
pub fn req_data_mut(&self) -> RefMut<'_, Extensions> {
|
||||||
|
self.req.req_data_mut()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
|
#[inline]
|
||||||
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
||||||
self.req.cookies()
|
self.req.cookies()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return request cookie.
|
/// Return request cookie.
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
|
#[inline]
|
||||||
pub fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
|
pub fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
|
||||||
self.req.cookie(name)
|
self.req.cookie(name)
|
||||||
}
|
}
|
||||||
|
@ -283,6 +297,14 @@ impl ServiceRequest {
|
||||||
.app_data
|
.app_data
|
||||||
.push(extensions);
|
.push(extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a context object for use with a [guard](crate::guard).
|
||||||
|
///
|
||||||
|
/// Useful if you are implementing
|
||||||
|
#[inline]
|
||||||
|
pub fn guard_ctx(&self) -> GuardContext<'_> {
|
||||||
|
GuardContext { req: self }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resource<Url> for ServiceRequest {
|
impl Resource<Url> for ServiceRequest {
|
||||||
|
@ -293,7 +315,7 @@ impl Resource<Url> for ServiceRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpMessage for ServiceRequest {
|
impl HttpMessage for ServiceRequest {
|
||||||
type Stream = PayloadStream;
|
type Stream = BoxedPayloadStream;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Returns Request's headers.
|
/// Returns Request's headers.
|
||||||
|
|
|
@ -124,7 +124,7 @@ impl TestRequest {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set HTTP Uri of this request
|
/// Set HTTP URI of this request
|
||||||
pub fn uri(mut self, path: &str) -> Self {
|
pub fn uri(mut self, path: &str) -> Self {
|
||||||
self.req.uri(path);
|
self.req.uri(path);
|
||||||
self
|
self
|
||||||
|
@ -174,25 +174,28 @@ impl TestRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set request payload.
|
/// Set request payload.
|
||||||
pub fn set_payload<B: Into<Bytes>>(mut self, data: B) -> Self {
|
pub fn set_payload(mut self, data: impl Into<Bytes>) -> Self {
|
||||||
self.req.set_payload(data);
|
self.req.set_payload(data);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize `data` to a URL encoded form and set it as the request payload. The `Content-Type`
|
/// Serialize `data` to a URL encoded form and set it as the request payload.
|
||||||
/// header is set to `application/x-www-form-urlencoded`.
|
///
|
||||||
pub fn set_form<T: Serialize>(mut self, data: &T) -> Self {
|
/// The `Content-Type` header is set to `application/x-www-form-urlencoded`.
|
||||||
let bytes = serde_urlencoded::to_string(data)
|
pub fn set_form(mut self, data: impl Serialize) -> Self {
|
||||||
|
let bytes = serde_urlencoded::to_string(&data)
|
||||||
.expect("Failed to serialize test data as a urlencoded form");
|
.expect("Failed to serialize test data as a urlencoded form");
|
||||||
self.req.set_payload(bytes);
|
self.req.set_payload(bytes);
|
||||||
self.req.insert_header(ContentType::form_url_encoded());
|
self.req.insert_header(ContentType::form_url_encoded());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is
|
/// Serialize `data` to JSON and set it as the request payload.
|
||||||
/// set to `application/json`.
|
///
|
||||||
pub fn set_json<T: Serialize>(mut self, data: &T) -> Self {
|
/// The `Content-Type` header is set to `application/json`.
|
||||||
let bytes = serde_json::to_string(data).expect("Failed to serialize test data to json");
|
pub fn set_json(mut self, data: impl Serialize) -> Self {
|
||||||
|
let bytes =
|
||||||
|
serde_json::to_string(&data).expect("Failed to serialize test data to json");
|
||||||
self.req.set_payload(bytes);
|
self.req.set_payload(bytes);
|
||||||
self.req.insert_header(ContentType::json());
|
self.req.insert_header(ContentType::json());
|
||||||
self
|
self
|
||||||
|
|
30
src/web.rs
30
src/web.rs
|
@ -1,14 +1,14 @@
|
||||||
//! Essentials helper functions and types for application registration.
|
//! Essentials helper functions and types for application registration.
|
||||||
|
|
||||||
use std::{error::Error as StdError, future::Future};
|
use std::future::Future;
|
||||||
|
|
||||||
use actix_http::Method;
|
use actix_http::Method;
|
||||||
use actix_router::IntoPatterns;
|
use actix_router::IntoPatterns;
|
||||||
pub use bytes::{Buf, BufMut, Bytes, BytesMut};
|
pub use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::MessageBody, error::BlockingError, extract::FromRequest, handler::Handler,
|
error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource,
|
||||||
resource::Resource, route::Route, scope::Scope, service::WebService, Responder,
|
route::Route, scope::Scope, service::WebService, Responder,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::config::ServiceConfig;
|
pub use crate::config::ServiceConfig;
|
||||||
|
@ -52,11 +52,16 @@ pub fn resource<T: IntoPatterns>(path: T) -> Resource {
|
||||||
/// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic
|
/// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic
|
||||||
/// path segments.
|
/// path segments.
|
||||||
///
|
///
|
||||||
|
/// # Avoid Trailing Slashes
|
||||||
|
/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost
|
||||||
|
/// certainly not have the expected behavior. See the [documentation on resource definitions][pat]
|
||||||
|
/// to understand why this is the case and how to correctly construct scope/prefix definitions.
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// In this example, three routes are set up (and will handle any method):
|
/// In this example, three routes are set up (and will handle any method):
|
||||||
/// * `/{project_id}/path1`
|
/// - `/{project_id}/path1`
|
||||||
/// * `/{project_id}/path2`
|
/// - `/{project_id}/path2`
|
||||||
/// * `/{project_id}/path3`
|
/// - `/{project_id}/path3`
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, App, HttpResponse};
|
/// use actix_web::{web, App, HttpResponse};
|
||||||
|
@ -68,6 +73,8 @@ pub fn resource<T: IntoPatterns>(path: T) -> Resource {
|
||||||
/// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed()))
|
/// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed()))
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// [pat]: crate::dev::ResourceDef#prefix-resources
|
||||||
pub fn scope(path: &str) -> Scope {
|
pub fn scope(path: &str) -> Scope {
|
||||||
Scope::new(path)
|
Scope::new(path)
|
||||||
}
|
}
|
||||||
|
@ -139,14 +146,11 @@ pub fn method(method: Method) -> Route {
|
||||||
/// web::to(index))
|
/// web::to(index))
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub fn to<F, I, R>(handler: F) -> Route
|
pub fn to<F, Args>(handler: F) -> Route
|
||||||
where
|
where
|
||||||
F: Handler<I, R>,
|
F: Handler<Args>,
|
||||||
I: FromRequest + 'static,
|
Args: FromRequest + 'static,
|
||||||
R: Future + 'static,
|
F::Output: Responder + 'static,
|
||||||
R::Output: Responder + 'static,
|
|
||||||
<R::Output as Responder>::Body: MessageBody + 'static,
|
|
||||||
<<R::Output as Responder>::Body as MessageBody>::Error: Into<Box<dyn StdError + 'static>>,
|
|
||||||
{
|
{
|
||||||
Route::new().to(handler)
|
Route::new().to(handler)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue