mirror of https://github.com/fafhrd91/actix-web
Merge branch 'master' into extract-from-service-request
This commit is contained in:
commit
59c6db1edc
|
@ -78,15 +78,6 @@ jobs:
|
||||||
cargo test --lib --tests -p=actix-multipart --all-features
|
cargo test --lib --tests -p=actix-multipart --all-features
|
||||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||||
|
|
||||||
- name: tests (io-uring)
|
|
||||||
if: matrix.target.os == 'ubuntu-latest'
|
|
||||||
timeout-minutes: 60
|
|
||||||
run: >
|
|
||||||
sudo bash -c "ulimit -Sl 512
|
|
||||||
&& ulimit -Hl 512
|
|
||||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
|
||||||
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
- name: Clear the cargo caches
|
||||||
run: |
|
run: |
|
||||||
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
|
||||||
|
@ -125,34 +116,6 @@ jobs:
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-check-all-feature-powerset-linux }
|
with: { command: ci-check-all-feature-powerset-linux }
|
||||||
|
|
||||||
# job currently (1st Feb 2022) segfaults
|
|
||||||
# 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 }
|
|
||||||
|
|
||||||
nextest:
|
nextest:
|
||||||
name: nextest
|
name: nextest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -81,19 +81,37 @@ jobs:
|
||||||
cargo test --lib --tests -p=actix-multipart --all-features
|
cargo test --lib --tests -p=actix-multipart --all-features
|
||||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||||
|
|
||||||
|
- name: Clear the cargo caches
|
||||||
|
run: |
|
||||||
|
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||||
|
cargo-cache
|
||||||
|
|
||||||
|
io-uring:
|
||||||
|
name: io-uring tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
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.3.0
|
||||||
|
|
||||||
- name: tests (io-uring)
|
- name: tests (io-uring)
|
||||||
if: matrix.target.os == 'ubuntu-latest'
|
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
run: >
|
run: >
|
||||||
sudo bash -c "ulimit -Sl 512
|
sudo bash -c "ulimit -Sl 512
|
||||||
&& ulimit -Hl 512
|
&& ulimit -Hl 512
|
||||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||||
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
&& RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
|
||||||
run: |
|
|
||||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
|
|
||||||
rustdoc:
|
rustdoc:
|
||||||
name: doc tests
|
name: doc tests
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
# disabled because `cargo tarpaulin` currently segfaults
|
||||||
|
|
||||||
|
name: Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# job currently (1st Feb 2022) segfaults
|
||||||
|
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 }
|
|
@ -1,6 +1,12 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021]
|
||||||
|
- Update `tokio-uring` dependency to `0.3`.
|
||||||
|
- Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645]
|
||||||
|
|
||||||
|
[#2021]: https://github.com/actix/actix-web/pull/2021
|
||||||
|
[#2645]: https://github.com/actix/actix-web/pull/2645
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0 - 2022-02-25
|
## 0.6.0 - 2022-02-25
|
||||||
|
|
|
@ -39,10 +39,12 @@ mime_guess = "2.0.1"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2.7"
|
pin-project-lite = "0.2.7"
|
||||||
|
|
||||||
|
# experimental-io-uring
|
||||||
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
|
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
|
||||||
|
actix-server = "2.1" # ensure matching tokio-uring versions
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.7"
|
||||||
actix-test = "0.1.0-beta.13"
|
actix-test = "0.1.0-beta.13"
|
||||||
actix-web = "4.0.0"
|
actix-web = "4.0.0"
|
||||||
tempfile = "3.2"
|
tempfile = "3.2"
|
||||||
|
|
|
@ -364,20 +364,43 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_status_code_text() {
|
async fn status_code_customize_same_output() {
|
||||||
let mut file = NamedFile::open_async("Cargo.toml")
|
let file1 = NamedFile::open_async("Cargo.toml")
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_status_code(StatusCode::NOT_FOUND);
|
.set_status_code(StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let file2 = NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.customize()
|
||||||
|
.with_status(StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res1 = file1.respond_to(&req);
|
||||||
|
let res2 = file2.respond_to(&req);
|
||||||
|
|
||||||
|
assert_eq!(res1.status(), StatusCode::NOT_FOUND);
|
||||||
|
assert_eq!(res2.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_named_file_status_code_text() {
|
||||||
|
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
|
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let _f: &mut File = &mut file;
|
let _f: &mut File = &mut file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let file = file.customize().with_status(StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req);
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -23,6 +23,7 @@ use actix_web::{
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
use mime::Mime;
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
|
|
||||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||||
|
@ -76,8 +77,8 @@ pub struct NamedFile {
|
||||||
pub(crate) md: Metadata,
|
pub(crate) md: Metadata,
|
||||||
pub(crate) flags: Flags,
|
pub(crate) flags: Flags,
|
||||||
pub(crate) status_code: StatusCode,
|
pub(crate) status_code: StatusCode,
|
||||||
pub(crate) content_type: mime::Mime,
|
pub(crate) content_type: Mime,
|
||||||
pub(crate) content_disposition: header::ContentDisposition,
|
pub(crate) content_disposition: ContentDisposition,
|
||||||
pub(crate) encoding: Option<ContentEncoding>,
|
pub(crate) encoding: Option<ContentEncoding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +129,7 @@ impl NamedFile {
|
||||||
let ct = from_path(&path).first_or_octet_stream();
|
let ct = from_path(&path).first_or_octet_stream();
|
||||||
|
|
||||||
let disposition = match ct.type_() {
|
let disposition = match ct.type_() {
|
||||||
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
|
||||||
mime::APPLICATION => match ct.subtype() {
|
mime::APPLICATION => match ct.subtype() {
|
||||||
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
||||||
name if name == "wasm" => DispositionType::Inline,
|
name if name == "wasm" => DispositionType::Inline,
|
||||||
|
@ -209,11 +210,10 @@ impl NamedFile {
|
||||||
Self::from_file(file, path)
|
Self::from_file(file, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(rustdoc::broken_intra_doc_links)]
|
|
||||||
/// Attempts to open a file asynchronously in read-only mode.
|
/// Attempts to open a file asynchronously in read-only mode.
|
||||||
///
|
///
|
||||||
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
|
/// When the `experimental-io-uring` crate feature is enabled, this will be async. Otherwise, it
|
||||||
/// Otherwise, it will be just like [`open`][Self::open].
|
/// will behave just like `open`.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -238,13 +238,13 @@ impl NamedFile {
|
||||||
Self::from_file(file, path)
|
Self::from_file(file, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns reference to the underlying `File` object.
|
/// Returns reference to the underlying file object.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn file(&self) -> &File {
|
pub fn file(&self) -> &File {
|
||||||
&self.file
|
&self.file
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the path of this file.
|
/// Returns the filesystem path to this file.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -262,16 +262,53 @@ impl NamedFile {
|
||||||
self.path.as_path()
|
self.path.as_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set response **Status Code**
|
/// Returns the time the file was last modified.
|
||||||
|
///
|
||||||
|
/// Returns `None` only on unsupported platforms; see [`std::fs::Metadata::modified()`].
|
||||||
|
/// Therefore, it is usually safe to unwrap this.
|
||||||
|
#[inline]
|
||||||
|
pub fn modified(&self) -> Option<SystemTime> {
|
||||||
|
self.modified
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the filesystem metadata associated with this file.
|
||||||
|
#[inline]
|
||||||
|
pub fn metadata(&self) -> &Metadata {
|
||||||
|
&self.md
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `Content-Type` header that will be used when serving this file.
|
||||||
|
#[inline]
|
||||||
|
pub fn content_type(&self) -> &Mime {
|
||||||
|
&self.content_type
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `Content-Disposition` that will be used when serving this file.
|
||||||
|
#[inline]
|
||||||
|
pub fn content_disposition(&self) -> &ContentDisposition {
|
||||||
|
&self.content_disposition
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `Content-Encoding` that will be used when serving this file.
|
||||||
|
///
|
||||||
|
/// A return value of `None` indicates that the content is not already using a compressed
|
||||||
|
/// representation and may be subject to compression downstream.
|
||||||
|
#[inline]
|
||||||
|
pub fn content_encoding(&self) -> Option<ContentEncoding> {
|
||||||
|
self.encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set response status code.
|
||||||
|
#[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")]
|
||||||
pub fn set_status_code(mut self, status: StatusCode) -> Self {
|
pub fn set_status_code(mut self, status: StatusCode) -> Self {
|
||||||
self.status_code = status;
|
self.status_code = status;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred
|
/// Sets the `Content-Type` header that will be used when serving this file. By default the
|
||||||
/// from the filename extension.
|
/// `Content-Type` is inferred from the filename extension.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
pub fn set_content_type(mut self, mime_type: Mime) -> Self {
|
||||||
self.content_type = mime_type;
|
self.content_type = mime_type;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -284,15 +321,15 @@ impl NamedFile {
|
||||||
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
||||||
/// (using `to_string_lossy`).
|
/// (using `to_string_lossy`).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self {
|
||||||
self.content_disposition = cd;
|
self.content_disposition = cd;
|
||||||
self.flags.insert(Flags::CONTENT_DISPOSITION);
|
self.flags.insert(Flags::CONTENT_DISPOSITION);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disable `Content-Disposition` header.
|
/// Disables `Content-Disposition` header.
|
||||||
///
|
///
|
||||||
/// By default Content-Disposition` header is enabled.
|
/// By default, the `Content-Disposition` header is sent.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn disable_content_disposition(mut self) -> Self {
|
pub fn disable_content_disposition(mut self) -> Self {
|
||||||
self.flags.remove(Flags::CONTENT_DISPOSITION);
|
self.flags.remove(Flags::CONTENT_DISPOSITION);
|
||||||
|
|
|
@ -29,13 +29,13 @@ default = []
|
||||||
openssl = ["tls-openssl", "awc/openssl"]
|
openssl = ["tls-openssl", "awc/openssl"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2.0.0"
|
actix-service = "2"
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-tls = "3"
|
actix-tls = "3"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3"
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-server = "2"
|
actix-server = "2"
|
||||||
awc = { version = "3.0.0-beta.21", default-features = false }
|
awc = { version = "3", default-features = false }
|
||||||
|
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||||
tokio = { version = "1.8.4", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||||
actix-http = "3.0.0"
|
actix-http = "3"
|
||||||
|
|
|
@ -3,6 +3,30 @@
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.4 - 2022-03-09
|
||||||
|
### Fixed
|
||||||
|
- Document on docs.rs with `ws` feature enabled.
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.3 - 2022-03-08
|
||||||
|
### Fixed
|
||||||
|
- Allow spaces between header name and colon when parsing responses. [#2684]
|
||||||
|
|
||||||
|
[#2684]: https://github.com/actix/actix-web/issues/2684
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.2 - 2022-03-05
|
||||||
|
### Fixed
|
||||||
|
- Fix encoding camel-case header names with more than one hyphen. [#2683]
|
||||||
|
|
||||||
|
[#2683]: https://github.com/actix/actix-web/issues/2683
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.1 - 2022-03-04
|
||||||
|
- Fix panic in H1 dispatcher when pipelining is used with keep-alive. [#2678]
|
||||||
|
|
||||||
|
[#2678]: https://github.com/actix/actix-web/issues/2678
|
||||||
|
|
||||||
## 3.0.0 - 2022-02-25
|
## 3.0.0 - 2022-02-25
|
||||||
### Dependencies
|
### Dependencies
|
||||||
- Updated `actix-*` to Tokio v1-based versions. [#1813]
|
- Updated `actix-*` to Tokio v1-based versions. [#1813]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.0.0"
|
version = "3.0.4"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
@ -20,7 +20,7 @@ edition = "2018"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
# features that docs.rs will build with
|
# features that docs.rs will build with
|
||||||
features = ["http2", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
features = ["http2", "ws", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_http"
|
name = "actix_http"
|
||||||
|
@ -73,11 +73,11 @@ httparse = "1.5.1"
|
||||||
httpdate = "1.0.1"
|
httpdate = "1.0.1"
|
||||||
itoa = "1"
|
itoa = "1"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
log = "0.4"
|
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
# http2
|
# http2
|
||||||
h2 = { version = "0.3.9", optional = true }
|
h2 = { version = "0.3.9", optional = true }
|
||||||
|
@ -94,7 +94,7 @@ actix-tls = { version = "3", default-features = false, optional = true }
|
||||||
# compress-*
|
# compress-*
|
||||||
brotli = { version = "3.3.3", optional = true }
|
brotli = { version = "3.3.3", optional = true }
|
||||||
flate2 = { version = "1.0.13", optional = true }
|
flate2 = { version = "1.0.13", optional = true }
|
||||||
zstd = { version = "0.10", optional = true }
|
zstd = { version = "0.11", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
|
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
|
||||||
|
@ -110,6 +110,7 @@ memchr = "2.4"
|
||||||
once_cell = "1.9"
|
once_cell = "1.9"
|
||||||
rcgen = "0.8"
|
rcgen = "0.8"
|
||||||
regex = "1.3"
|
regex = "1.3"
|
||||||
|
rustversion = "1"
|
||||||
rustls-pemfile = "0.2"
|
rustls-pemfile = "0.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
@ -120,7 +121,7 @@ tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "ws"
|
name = "ws"
|
||||||
required-features = ["rustls"]
|
required-features = ["ws", "rustls"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "write-camel-case"
|
name = "write-camel-case"
|
||||||
|
|
|
@ -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)
|
[](https://docs.rs/actix-http/3.0.4)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-http/3.0.0)
|
[](https://deps.rs/crate/actix-http/3.0.4)
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ use actix_http::{HttpService, Response};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use futures_util::future;
|
use futures_util::future;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use log::info;
|
use tracing::info;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
|
|
|
@ -18,7 +18,8 @@ async fn main() -> std::io::Result<()> {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
// pass the app to service builder
|
// pass the app to service builder
|
||||||
// map_config is used to map App's configuration to ServiceBuilder
|
// map_config is used to map App's configuration to ServiceBuilder
|
||||||
.finish(map_config(app, |_| AppConfig::default()))
|
// h1 will configure server to only use HTTP/1.1
|
||||||
|
.h1(map_config(app, |_| AppConfig::default()))
|
||||||
.tcp()
|
.tcp()
|
||||||
})?
|
})?
|
||||||
.run()
|
.run()
|
||||||
|
|
|
@ -5,6 +5,7 @@ use actix_server::Server;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use futures_util::StreamExt as _;
|
use futures_util::StreamExt as _;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
|
@ -22,7 +23,7 @@ async fn main() -> io::Result<()> {
|
||||||
body.extend_from_slice(&item?);
|
body.extend_from_slice(&item?);
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("request body: {:?}", body);
|
info!("request body: {:?}", body);
|
||||||
|
|
||||||
let res = Response::build(StatusCode::OK)
|
let res = Response::build(StatusCode::OK)
|
||||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use std::{convert::Infallible, io, time::Duration};
|
use std::{convert::Infallible, io, time::Duration};
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{header::HeaderValue, HttpService, Request, Response, StatusCode};
|
||||||
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
|
|
||||||
};
|
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
|
@ -18,12 +17,12 @@ async fn main() -> io::Result<()> {
|
||||||
ext.insert(42u32);
|
ext.insert(42u32);
|
||||||
})
|
})
|
||||||
.finish(|req: Request| async move {
|
.finish(|req: Request| async move {
|
||||||
log::info!("{:?}", req);
|
info!("{:?}", req);
|
||||||
|
|
||||||
let mut res = Response::build(StatusCode::OK);
|
let mut res = Response::build(StatusCode::OK);
|
||||||
res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
|
res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
|
||||||
|
|
||||||
let forty_two = req.extensions().get::<u32>().unwrap().to_string();
|
let forty_two = req.conn_data::<u32>().unwrap().to_string();
|
||||||
res.insert_header((
|
res.insert_header((
|
||||||
"x-forty-two",
|
"x-forty-two",
|
||||||
HeaderValue::from_str(&forty_two).unwrap(),
|
HeaderValue::from_str(&forty_two).unwrap(),
|
||||||
|
|
|
@ -12,6 +12,7 @@ use actix_http::{body::BodyStream, HttpService, Response};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
|
@ -21,7 +22,7 @@ async fn main() -> io::Result<()> {
|
||||||
.bind("streaming-error", ("127.0.0.1", 8080), || {
|
.bind("streaming-error", ("127.0.0.1", 8080), || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.finish(|req| async move {
|
.finish(|req| async move {
|
||||||
log::info!("{:?}", req);
|
info!("{:?}", req);
|
||||||
let res = Response::ok();
|
let res = Response::ok();
|
||||||
|
|
||||||
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
|
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
|
||||||
|
|
|
@ -17,6 +17,7 @@ use actix_server::Server;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use bytestring::ByteString;
|
use bytestring::ByteString;
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
|
use tracing::{info, trace};
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
|
@ -34,13 +35,13 @@ async fn main() -> io::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
|
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
|
||||||
log::info!("handshaking");
|
info!("handshaking");
|
||||||
let mut res = ws::handshake(req.head())?;
|
let mut res = ws::handshake(req.head())?;
|
||||||
|
|
||||||
// handshake will always fail under HTTP/2
|
// handshake will always fail under HTTP/2
|
||||||
|
|
||||||
log::info!("responding");
|
info!("responding");
|
||||||
Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?)
|
res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Heartbeat {
|
struct Heartbeat {
|
||||||
|
@ -61,7 +62,7 @@ impl Stream for Heartbeat {
|
||||||
type Item = Result<Bytes, Error>;
|
type Item = Result<Bytes, Error>;
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
log::trace!("poll");
|
trace!("poll");
|
||||||
|
|
||||||
ready!(self.as_mut().interval.poll_tick(cx));
|
ready!(self.as_mut().interval.poll_tick(cx));
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ use pin_project_lite::pin_project;
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||||
|
|
||||||
|
use tracing::trace;
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
use zstd::stream::write::Encoder as ZstdEncoder;
|
use zstd::stream::write::Encoder as ZstdEncoder;
|
||||||
|
|
||||||
|
@ -356,7 +357,7 @@ impl ContentEncoder {
|
||||||
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("Error decoding br encoding: {}", err);
|
trace!("Error decoding br encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -365,7 +366,7 @@ impl ContentEncoder {
|
||||||
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("Error decoding gzip encoding: {}", err);
|
trace!("Error decoding gzip encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -374,7 +375,7 @@ impl ContentEncoder {
|
||||||
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("Error decoding deflate encoding: {}", err);
|
trace!("Error decoding deflate encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -383,7 +384,7 @@ impl ContentEncoder {
|
||||||
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("Error decoding ztsd encoding: {}", err);
|
trace!("Error decoding ztsd encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{io, task::Poll};
|
use std::{io, task::Poll};
|
||||||
|
|
||||||
use bytes::{Buf as _, Bytes, BytesMut};
|
use bytes::{Buf as _, Bytes, BytesMut};
|
||||||
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
macro_rules! byte (
|
macro_rules! byte (
|
||||||
($rdr:ident) => ({
|
($rdr:ident) => ({
|
||||||
|
@ -76,7 +77,7 @@ impl ChunkedState {
|
||||||
Poll::Ready(Ok(ChunkedState::Size))
|
Poll::Ready(Ok(ChunkedState::Size))
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::debug!("chunk size would overflow u64");
|
debug!("chunk size would overflow u64");
|
||||||
Poll::Ready(Err(io::Error::new(
|
Poll::Ready(Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
"Invalid chunk size line: Size is too big",
|
"Invalid chunk size line: Size is too big",
|
||||||
|
@ -124,7 +125,7 @@ impl ChunkedState {
|
||||||
rem: &mut u64,
|
rem: &mut u64,
|
||||||
buf: &mut Option<Bytes>,
|
buf: &mut Option<Bytes>,
|
||||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
log::trace!("Chunked read, remaining={:?}", rem);
|
trace!("Chunked read, remaining={:?}", rem);
|
||||||
|
|
||||||
let len = rdr.len() as u64;
|
let len = rdr.len() as u64;
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
|
|
|
@ -6,7 +6,7 @@ use http::{
|
||||||
header::{self, HeaderName, HeaderValue},
|
header::{self, HeaderName, HeaderValue},
|
||||||
Method, StatusCode, Uri, Version,
|
Method, StatusCode, Uri, Version,
|
||||||
};
|
};
|
||||||
use log::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
use super::chunked::ChunkedState;
|
use super::chunked::ChunkedState;
|
||||||
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead};
|
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead};
|
||||||
|
@ -293,22 +293,35 @@ impl MessageType for ResponseHead {
|
||||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||||
|
|
||||||
let (len, ver, status, h_len) = {
|
let (len, ver, status, h_len) = {
|
||||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
// SAFETY:
|
||||||
|
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is safe because the
|
||||||
|
// type we are claiming to have initialized here is a bunch of `MaybeUninit`s, which
|
||||||
|
// do not require initialization.
|
||||||
|
let mut parsed = unsafe {
|
||||||
|
MaybeUninit::<[MaybeUninit<httparse::Header<'_>>; MAX_HEADERS]>::uninit()
|
||||||
|
.assume_init()
|
||||||
|
};
|
||||||
|
|
||||||
let mut res = httparse::Response::new(&mut parsed);
|
let mut res = httparse::Response::new(&mut []);
|
||||||
match res.parse(src)? {
|
|
||||||
|
let mut config = httparse::ParserConfig::default();
|
||||||
|
config.allow_spaces_after_header_name_in_responses(true);
|
||||||
|
|
||||||
|
match config.parse_response_with_uninit_headers(&mut res, src, &mut parsed)? {
|
||||||
httparse::Status::Complete(len) => {
|
httparse::Status::Complete(len) => {
|
||||||
let version = if res.version.unwrap() == 1 {
|
let version = if res.version.unwrap() == 1 {
|
||||||
Version::HTTP_11
|
Version::HTTP_11
|
||||||
} else {
|
} else {
|
||||||
Version::HTTP_10
|
Version::HTTP_10
|
||||||
};
|
};
|
||||||
|
|
||||||
let status = StatusCode::from_u16(res.code.unwrap())
|
let status = StatusCode::from_u16(res.code.unwrap())
|
||||||
.map_err(|_| ParseError::Status)?;
|
.map_err(|_| ParseError::Status)?;
|
||||||
HeaderIndex::record(src, res.headers, &mut headers);
|
HeaderIndex::record(src, res.headers, &mut headers);
|
||||||
|
|
||||||
(len, version, status, res.headers.len())
|
(len, version, status, res.headers.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
httparse::Status::Partial => {
|
httparse::Status::Partial => {
|
||||||
return if src.len() >= MAX_BUFFER_SIZE {
|
return if src.len() >= MAX_BUFFER_SIZE {
|
||||||
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||||
|
@ -360,9 +373,6 @@ pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
||||||
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||||
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||||
|
|
||||||
pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
|
||||||
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
|
||||||
|
|
||||||
impl HeaderIndex {
|
impl HeaderIndex {
|
||||||
pub(crate) fn record(
|
pub(crate) fn record(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
|
|
|
@ -15,6 +15,7 @@ use bitflags::bitflags;
|
||||||
use bytes::{Buf, BytesMut};
|
use bytes::{Buf, BytesMut};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BodySize, BoxBody, MessageBody},
|
body::{BodySize, BoxBody, MessageBody},
|
||||||
|
@ -336,7 +337,7 @@ where
|
||||||
while written < len {
|
while written < len {
|
||||||
match io.as_mut().poll_write(cx, &write_buf[written..])? {
|
match io.as_mut().poll_write(cx, &write_buf[written..])? {
|
||||||
Poll::Ready(0) => {
|
Poll::Ready(0) => {
|
||||||
log::error!("write zero; closing");
|
error!("write zero; closing");
|
||||||
return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, "")));
|
return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, "")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,8 +376,6 @@ where
|
||||||
DispatchError::Io(err)
|
DispatchError::Io(err)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive());
|
|
||||||
|
|
||||||
Ok(size)
|
Ok(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,7 +458,12 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// all messages are dealt with
|
// all messages are dealt with
|
||||||
None => return Ok(PollResponse::DoNothing),
|
None => {
|
||||||
|
// start keep-alive if last request allowed it
|
||||||
|
this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive());
|
||||||
|
|
||||||
|
return Ok(PollResponse::DoNothing);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
StateProj::ServiceCall { fut } => {
|
StateProj::ServiceCall { fut } => {
|
||||||
|
@ -565,7 +569,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
StateProj::ExpectCall { fut } => {
|
StateProj::ExpectCall { fut } => {
|
||||||
log::trace!(" calling expect service");
|
trace!(" calling expect service");
|
||||||
|
|
||||||
match fut.poll(cx) {
|
match fut.poll(cx) {
|
||||||
// expect resolved. write continue to buffer and set InnerDispatcher state
|
// expect resolved. write continue to buffer and set InnerDispatcher state
|
||||||
|
@ -694,12 +698,12 @@ where
|
||||||
let mut this = self.as_mut().project();
|
let mut this = self.as_mut().project();
|
||||||
|
|
||||||
if can_not_read {
|
if can_not_read {
|
||||||
log::debug!("cannot read request payload");
|
debug!("cannot read request payload");
|
||||||
|
|
||||||
if let Some(sender) = &this.payload {
|
if let Some(sender) = &this.payload {
|
||||||
// ...maybe handler does not want to read any more payload...
|
// ...maybe handler does not want to read any more payload...
|
||||||
if let PayloadStatus::Dropped = sender.need_read(cx) {
|
if let PayloadStatus::Dropped = sender.need_read(cx) {
|
||||||
log::debug!("handler dropped payload early; attempt to clean connection");
|
debug!("handler dropped payload early; attempt to clean connection");
|
||||||
// ...in which case poll request payload a few times
|
// ...in which case poll request payload a few times
|
||||||
loop {
|
loop {
|
||||||
match this.codec.decode(this.read_buf)? {
|
match this.codec.decode(this.read_buf)? {
|
||||||
|
@ -713,7 +717,7 @@ where
|
||||||
|
|
||||||
// connection is in clean state for next request
|
// connection is in clean state for next request
|
||||||
Message::Chunk(None) => {
|
Message::Chunk(None) => {
|
||||||
log::debug!("connection successfully cleaned");
|
debug!("connection successfully cleaned");
|
||||||
|
|
||||||
// reset dispatcher state
|
// reset dispatcher state
|
||||||
let _ = this.payload.take();
|
let _ = this.payload.take();
|
||||||
|
@ -734,7 +738,7 @@ where
|
||||||
|
|
||||||
// not enough info to decide if connection is going to be clean or not
|
// not enough info to decide if connection is going to be clean or not
|
||||||
None => {
|
None => {
|
||||||
log::error!(
|
error!(
|
||||||
"handler did not read whole payload and dispatcher could not \
|
"handler did not read whole payload and dispatcher could not \
|
||||||
drain read buf; return 500 and close connection"
|
drain read buf; return 500 and close connection"
|
||||||
);
|
);
|
||||||
|
@ -757,6 +761,7 @@ where
|
||||||
|
|
||||||
let mut updated = false;
|
let mut updated = false;
|
||||||
|
|
||||||
|
// decode from read buf as many full requests as possible
|
||||||
loop {
|
loop {
|
||||||
match this.codec.decode(this.read_buf) {
|
match this.codec.decode(this.read_buf) {
|
||||||
Ok(Some(msg)) => {
|
Ok(Some(msg)) => {
|
||||||
|
@ -809,7 +814,7 @@ where
|
||||||
if let Some(ref mut payload) = this.payload {
|
if let Some(ref mut payload) = this.payload {
|
||||||
payload.feed_data(chunk);
|
payload.feed_data(chunk);
|
||||||
} else {
|
} else {
|
||||||
log::error!("Internal server error: unexpected payload chunk");
|
error!("Internal server error: unexpected payload chunk");
|
||||||
this.flags.insert(Flags::READ_DISCONNECT);
|
this.flags.insert(Flags::READ_DISCONNECT);
|
||||||
this.messages.push_back(DispatcherMessage::Error(
|
this.messages.push_back(DispatcherMessage::Error(
|
||||||
Response::internal_server_error().drop_body(),
|
Response::internal_server_error().drop_body(),
|
||||||
|
@ -823,7 +828,7 @@ where
|
||||||
if let Some(mut payload) = this.payload.take() {
|
if let Some(mut payload) = this.payload.take() {
|
||||||
payload.feed_eof();
|
payload.feed_eof();
|
||||||
} else {
|
} else {
|
||||||
log::error!("Internal server error: unexpected eof");
|
error!("Internal server error: unexpected eof");
|
||||||
this.flags.insert(Flags::READ_DISCONNECT);
|
this.flags.insert(Flags::READ_DISCONNECT);
|
||||||
this.messages.push_back(DispatcherMessage::Error(
|
this.messages.push_back(DispatcherMessage::Error(
|
||||||
Response::internal_server_error().drop_body(),
|
Response::internal_server_error().drop_body(),
|
||||||
|
@ -840,7 +845,7 @@ where
|
||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
|
|
||||||
Err(ParseError::Io(err)) => {
|
Err(ParseError::Io(err)) => {
|
||||||
log::trace!("I/O error: {}", &err);
|
trace!("I/O error: {}", &err);
|
||||||
self.as_mut().client_disconnected();
|
self.as_mut().client_disconnected();
|
||||||
this = self.as_mut().project();
|
this = self.as_mut().project();
|
||||||
*this.error = Some(DispatchError::Io(err));
|
*this.error = Some(DispatchError::Io(err));
|
||||||
|
@ -848,7 +853,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(ParseError::TooLarge) => {
|
Err(ParseError::TooLarge) => {
|
||||||
log::trace!("request head was too big; returning 431 response");
|
trace!("request head was too big; returning 431 response");
|
||||||
|
|
||||||
if let Some(mut payload) = this.payload.take() {
|
if let Some(mut payload) = this.payload.take() {
|
||||||
payload.set_error(PayloadError::Overflow);
|
payload.set_error(PayloadError::Overflow);
|
||||||
|
@ -868,7 +873,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("parse error {}", &err);
|
trace!("parse error {}", &err);
|
||||||
|
|
||||||
if let Some(mut payload) = this.payload.take() {
|
if let Some(mut payload) = this.payload.take() {
|
||||||
payload.set_error(PayloadError::EncodingCorrupted);
|
payload.set_error(PayloadError::EncodingCorrupted);
|
||||||
|
@ -899,7 +904,7 @@ where
|
||||||
if timer.as_mut().poll(cx).is_ready() {
|
if timer.as_mut().poll(cx).is_ready() {
|
||||||
// timeout on first request (slow request) return 408
|
// timeout on first request (slow request) return 408
|
||||||
|
|
||||||
log::trace!(
|
trace!(
|
||||||
"timed out on slow request; \
|
"timed out on slow request; \
|
||||||
replying with 408 and closing connection"
|
replying with 408 and closing connection"
|
||||||
);
|
);
|
||||||
|
@ -945,7 +950,7 @@ where
|
||||||
// keep-alive timer has timed out
|
// keep-alive timer has timed out
|
||||||
if timer.as_mut().poll(cx).is_ready() {
|
if timer.as_mut().poll(cx).is_ready() {
|
||||||
// no tasks at hand
|
// no tasks at hand
|
||||||
log::trace!("timer timed out; closing connection");
|
trace!("timer timed out; closing connection");
|
||||||
this.flags.insert(Flags::SHUTDOWN);
|
this.flags.insert(Flags::SHUTDOWN);
|
||||||
|
|
||||||
if let Some(deadline) = this.config.client_disconnect_deadline() {
|
if let Some(deadline) = this.config.client_disconnect_deadline() {
|
||||||
|
@ -975,7 +980,7 @@ where
|
||||||
|
|
||||||
// timed-out during shutdown; drop connection
|
// timed-out during shutdown; drop connection
|
||||||
if timer.as_mut().poll(cx).is_ready() {
|
if timer.as_mut().poll(cx).is_ready() {
|
||||||
log::trace!("timed-out during shutdown");
|
trace!("timed-out during shutdown");
|
||||||
return Err(DispatchError::DisconnectTimeout);
|
return Err(DispatchError::DisconnectTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1134,12 +1139,12 @@ where
|
||||||
|
|
||||||
match this.inner.project() {
|
match this.inner.project() {
|
||||||
DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
|
DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
|
||||||
log::error!("Upgrade handler error: {}", err);
|
error!("Upgrade handler error: {}", err);
|
||||||
DispatchError::Upgrade
|
DispatchError::Upgrade
|
||||||
}),
|
}),
|
||||||
|
|
||||||
DispatcherStateProj::Normal { mut inner } => {
|
DispatcherStateProj::Normal { mut inner } => {
|
||||||
log::trace!("start flags: {:?}", &inner.flags);
|
trace!("start flags: {:?}", &inner.flags);
|
||||||
|
|
||||||
trace_timer_states(
|
trace_timer_states(
|
||||||
"start",
|
"start",
|
||||||
|
@ -1246,7 +1251,7 @@ where
|
||||||
|
|
||||||
// client is gone
|
// client is gone
|
||||||
if inner.flags.contains(Flags::WRITE_DISCONNECT) {
|
if inner.flags.contains(Flags::WRITE_DISCONNECT) {
|
||||||
log::trace!("client is gone; disconnecting");
|
trace!("client is gone; disconnecting");
|
||||||
return Poll::Ready(Ok(()));
|
return Poll::Ready(Ok(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1255,14 +1260,14 @@ where
|
||||||
|
|
||||||
// read half is closed; we do not process any responses
|
// read half is closed; we do not process any responses
|
||||||
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none {
|
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none {
|
||||||
log::trace!("read half closed; start shutdown");
|
trace!("read half closed; start shutdown");
|
||||||
inner_p.flags.insert(Flags::SHUTDOWN);
|
inner_p.flags.insert(Flags::SHUTDOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-alive and stream errors
|
// keep-alive and stream errors
|
||||||
if state_is_none && inner_p.write_buf.is_empty() {
|
if state_is_none && inner_p.write_buf.is_empty() {
|
||||||
if let Some(err) = inner_p.error.take() {
|
if let Some(err) = inner_p.error.take() {
|
||||||
log::error!("stream error: {}", &err);
|
error!("stream error: {}", &err);
|
||||||
return Poll::Ready(Err(err));
|
return Poll::Ready(Err(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1291,7 +1296,7 @@ where
|
||||||
Poll::Pending
|
Poll::Pending
|
||||||
};
|
};
|
||||||
|
|
||||||
log::trace!("end flags: {:?}", &inner.flags);
|
trace!("end flags: {:?}", &inner.flags);
|
||||||
|
|
||||||
poll
|
poll
|
||||||
}
|
}
|
||||||
|
@ -1306,17 +1311,17 @@ fn trace_timer_states(
|
||||||
ka_timer: &TimerState,
|
ka_timer: &TimerState,
|
||||||
shutdown_timer: &TimerState,
|
shutdown_timer: &TimerState,
|
||||||
) {
|
) {
|
||||||
log::trace!("{} timers:", label);
|
trace!("{} timers:", label);
|
||||||
|
|
||||||
if head_timer.is_enabled() {
|
if head_timer.is_enabled() {
|
||||||
log::trace!(" head {}", &head_timer);
|
trace!(" head {}", &head_timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ka_timer.is_enabled() {
|
if ka_timer.is_enabled() {
|
||||||
log::trace!(" keep-alive {}", &ka_timer);
|
trace!(" keep-alive {}", &ka_timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if shutdown_timer.is_enabled() {
|
if shutdown_timer.is_enabled() {
|
||||||
log::trace!(" shutdown {}", &shutdown_timer);
|
trace!(" shutdown {}", &shutdown_timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -517,6 +517,7 @@ unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
|
||||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||||
buffer[index] = c & 0b1101_1111;
|
buffer[index] = c & 0b1101_1111;
|
||||||
}
|
}
|
||||||
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
index += 1;
|
index += 1;
|
||||||
|
@ -528,7 +529,7 @@ mod tests {
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http::header::AUTHORIZATION;
|
use http::header::{AUTHORIZATION, UPGRADE_INSECURE_REQUESTS};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -559,6 +560,9 @@ mod tests {
|
||||||
head.headers
|
head.headers
|
||||||
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
|
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
|
||||||
|
|
||||||
|
head.headers
|
||||||
|
.insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1"));
|
||||||
|
|
||||||
let mut head = RequestHeadType::Owned(head);
|
let mut head = RequestHeadType::Owned(head);
|
||||||
|
|
||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
|
@ -574,6 +578,7 @@ mod tests {
|
||||||
assert!(data.contains("Connection: close\r\n"));
|
assert!(data.contains("Connection: close\r\n"));
|
||||||
assert!(data.contains("Content-Type: plain/text\r\n"));
|
assert!(data.contains("Content-Type: plain/text\r\n"));
|
||||||
assert!(data.contains("Date: date\r\n"));
|
assert!(data.contains("Date: date\r\n"));
|
||||||
|
assert!(data.contains("Upgrade-Insecure-Requests: 1\r\n"));
|
||||||
|
|
||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
&mut bytes,
|
&mut bytes,
|
||||||
|
|
|
@ -263,7 +263,10 @@ mod tests {
|
||||||
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
assert_impl_all!(Inner: Unpin, Send, Sync);
|
assert_impl_all!(Inner: Unpin, Send, Sync);
|
||||||
|
#[rustversion::before(1.60)]
|
||||||
assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe);
|
assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe);
|
||||||
|
#[rustversion::since(1.60)]
|
||||||
|
assert_impl_all!(Inner: UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_unread_data() {
|
async fn test_unread_data() {
|
||||||
|
|
|
@ -13,6 +13,7 @@ use actix_service::{
|
||||||
};
|
};
|
||||||
use actix_utils::future::ready;
|
use actix_utils::future::ready;
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BoxBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
|
@ -305,13 +306,13 @@ where
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let expect = expect
|
let expect = expect
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
|
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
|
||||||
|
|
||||||
let upgrade = match upgrade {
|
let upgrade = match upgrade {
|
||||||
Some(upgrade) => {
|
Some(upgrade) => {
|
||||||
let upgrade = upgrade
|
let upgrade = upgrade
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?;
|
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
|
||||||
Some(upgrade)
|
Some(upgrade)
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
|
@ -319,7 +320,7 @@ where
|
||||||
|
|
||||||
let service = service
|
let service = service
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
|
.map_err(|e| error!("Init http service error: {:?}", e))?;
|
||||||
|
|
||||||
Ok(H1ServiceHandler::new(
|
Ok(H1ServiceHandler::new(
|
||||||
cfg,
|
cfg,
|
||||||
|
@ -357,7 +358,7 @@ where
|
||||||
|
|
||||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self._poll_ready(cx).map_err(|err| {
|
self._poll_ready(cx).map_err(|err| {
|
||||||
log::error!("HTTP/1 service readiness error: {:?}", err);
|
error!("HTTP/1 service readiness error: {:?}", err);
|
||||||
DispatchError::Service(err)
|
DispatchError::Service(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{fmt, future::Future, pin::Pin, task::Context};
|
use std::{fmt, future::Future, pin::Pin, task::Context};
|
||||||
|
|
||||||
use actix_rt::time::{Instant, Sleep};
|
use actix_rt::time::{Instant, Sleep};
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(super) enum TimerState {
|
pub(super) enum TimerState {
|
||||||
|
@ -24,7 +25,7 @@ impl TimerState {
|
||||||
|
|
||||||
pub(super) fn set(&mut self, timer: Sleep, line: u32) {
|
pub(super) fn set(&mut self, timer: Sleep, line: u32) {
|
||||||
if matches!(self, Self::Disabled) {
|
if matches!(self, Self::Disabled) {
|
||||||
log::trace!("setting disabled timer from line {}", line);
|
trace!("setting disabled timer from line {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
*self = Self::Active {
|
*self = Self::Active {
|
||||||
|
@ -39,11 +40,11 @@ impl TimerState {
|
||||||
|
|
||||||
pub(super) fn clear(&mut self, line: u32) {
|
pub(super) fn clear(&mut self, line: u32) {
|
||||||
if matches!(self, Self::Disabled) {
|
if matches!(self, Self::Disabled) {
|
||||||
log::trace!("trying to clear a disabled timer from line {}", line);
|
trace!("trying to clear a disabled timer from line {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(self, Self::Inactive) {
|
if matches!(self, Self::Inactive) {
|
||||||
log::trace!("trying to clear an inactive timer from line {}", line);
|
trace!("trying to clear an inactive timer from line {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
*self = Self::Inactive;
|
*self = Self::Inactive;
|
||||||
|
|
|
@ -19,8 +19,8 @@ use h2::{
|
||||||
server::{Connection, SendResponse},
|
server::{Connection, SendResponse},
|
||||||
Ping, PingPong,
|
Ping, PingPong,
|
||||||
};
|
};
|
||||||
use log::{error, trace};
|
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
use tracing::{error, trace, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BodySize, BoxBody, MessageBody},
|
body::{BodySize, BoxBody, MessageBody},
|
||||||
|
@ -143,7 +143,7 @@ where
|
||||||
DispatchError::SendResponse(err) => {
|
DispatchError::SendResponse(err) => {
|
||||||
trace!("Error sending HTTP/2 response: {:?}", err)
|
trace!("Error sending HTTP/2 response: {:?}", err)
|
||||||
}
|
}
|
||||||
DispatchError::SendData(err) => log::warn!("{:?}", err),
|
DispatchError::SendData(err) => warn!("{:?}", err),
|
||||||
DispatchError::ResponseBody(err) => {
|
DispatchError::ResponseBody(err) => {
|
||||||
error!("Response payload stream error: {:?}", err)
|
error!("Response payload stream error: {:?}", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use actix_service::{
|
||||||
};
|
};
|
||||||
use actix_utils::future::ready;
|
use actix_utils::future::ready;
|
||||||
use futures_core::{future::LocalBoxFuture, ready};
|
use futures_core::{future::LocalBoxFuture, ready};
|
||||||
use log::error;
|
use tracing::{error, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BoxBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
|
@ -355,7 +355,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("H2 handshake error: {}", err);
|
trace!("H2 handshake error: {}", err);
|
||||||
Poll::Ready(Err(err))
|
Poll::Ready(Err(err))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,7 +13,8 @@ use crate::error::PayloadError;
|
||||||
/// A boxed payload stream.
|
/// A boxed payload stream.
|
||||||
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
||||||
|
|
||||||
#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
#[doc(hidden)]
|
||||||
|
#[deprecated(since = "3.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
||||||
pub type PayloadStream = BoxedPayloadStream;
|
pub type PayloadStream = BoxedPayloadStream;
|
||||||
|
|
||||||
#[cfg(not(feature = "http2"))]
|
#[cfg(not(feature = "http2"))]
|
||||||
|
|
|
@ -144,7 +144,7 @@ impl ResponseBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set connection type to Upgrade
|
/// Set connection type to `Upgrade`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
|
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
|
||||||
where
|
where
|
||||||
|
@ -161,7 +161,7 @@ impl ResponseBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force close connection, even if it is marked as keep-alive
|
/// Force-close connection, even if it is marked as keep-alive.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn force_close(&mut self) -> &mut Self {
|
pub fn force_close(&mut self) -> &mut Self {
|
||||||
if let Some(parts) = self.inner() {
|
if let Some(parts) = self.inner() {
|
||||||
|
|
|
@ -15,6 +15,7 @@ use actix_service::{
|
||||||
};
|
};
|
||||||
use futures_core::{future::LocalBoxFuture, ready};
|
use futures_core::{future::LocalBoxFuture, ready};
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BoxBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
|
@ -369,13 +370,13 @@ where
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let expect = expect
|
let expect = expect
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
|
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
|
||||||
|
|
||||||
let upgrade = match upgrade {
|
let upgrade = match upgrade {
|
||||||
Some(upgrade) => {
|
Some(upgrade) => {
|
||||||
let upgrade = upgrade
|
let upgrade = upgrade
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?;
|
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
|
||||||
Some(upgrade)
|
Some(upgrade)
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
|
@ -383,7 +384,7 @@ where
|
||||||
|
|
||||||
let service = service
|
let service = service
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
|
.map_err(|e| error!("Init http service error: {:?}", e))?;
|
||||||
|
|
||||||
Ok(HttpServiceHandler::new(
|
Ok(HttpServiceHandler::new(
|
||||||
cfg,
|
cfg,
|
||||||
|
@ -490,7 +491,7 @@ where
|
||||||
|
|
||||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self._poll_ready(cx).map_err(|err| {
|
self._poll_ready(cx).map_err(|err| {
|
||||||
log::error!("HTTP service readiness error: {:?}", err);
|
error!("HTTP service readiness error: {:?}", err);
|
||||||
DispatchError::Service(err)
|
DispatchError::Service(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -666,7 +667,7 @@ where
|
||||||
self.poll(cx)
|
self.poll(cx)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("H2 handshake error: {}", err);
|
tracing::trace!("H2 handshake error: {}", err);
|
||||||
Poll::Ready(Err(err))
|
Poll::Ready(Err(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,29 +19,7 @@ use crate::{
|
||||||
Request,
|
Request,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Test `Request` builder
|
/// Test `Request` builder.
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// # use http::{header, StatusCode};
|
|
||||||
/// # use actix_web::*;
|
|
||||||
/// use actix_web::test::TestRequest;
|
|
||||||
///
|
|
||||||
/// fn index(req: &HttpRequest) -> Response {
|
|
||||||
/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
|
|
||||||
/// Response::Ok().into()
|
|
||||||
/// } else {
|
|
||||||
/// Response::BadRequest().into()
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let resp = TestRequest::default().insert_header("content-type", "text/plain")
|
|
||||||
/// .run(&index)
|
|
||||||
/// .unwrap();
|
|
||||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
|
||||||
///
|
|
||||||
/// let resp = TestRequest::default().run(&index).unwrap();
|
|
||||||
/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
||||||
/// ```
|
|
||||||
pub struct TestRequest(Option<Inner>);
|
pub struct TestRequest(Option<Inner>);
|
||||||
|
|
||||||
struct Inner {
|
struct Inner {
|
||||||
|
|
|
@ -2,6 +2,7 @@ use actix_codec::{Decoder, Encoder};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use bytestring::ByteString;
|
use bytestring::ByteString;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
frame::Parser,
|
frame::Parser,
|
||||||
|
@ -253,7 +254,7 @@ impl Decoder for Codec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::error!("Unfinished fragment {:?}", opcode);
|
error!("Unfinished fragment {:?}", opcode);
|
||||||
Err(ProtocolError::ContinuationFragment(opcode))
|
Err(ProtocolError::ContinuationFragment(opcode))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -73,8 +73,8 @@ mod inner {
|
||||||
use actix_service::{IntoService, Service};
|
use actix_service::{IntoService, Service};
|
||||||
use futures_core::stream::Stream;
|
use futures_core::stream::Stream;
|
||||||
use local_channel::mpsc;
|
use local_channel::mpsc;
|
||||||
use log::debug;
|
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
|
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use bytes::{Buf, BufMut, BytesMut};
|
use bytes::{Buf, BufMut, BytesMut};
|
||||||
use log::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
mask::apply_mask,
|
mask::apply_mask,
|
||||||
|
|
|
@ -3,6 +3,8 @@ use std::{
|
||||||
fmt,
|
fmt,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
/// Operation codes defined in [RFC 6455 §11.8].
|
/// Operation codes defined in [RFC 6455 §11.8].
|
||||||
///
|
///
|
||||||
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
|
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
|
||||||
|
@ -58,7 +60,7 @@ impl From<OpCode> for u8 {
|
||||||
Ping => 9,
|
Ping => 9,
|
||||||
Pong => 10,
|
Pong => 10,
|
||||||
Bad => {
|
Bad => {
|
||||||
log::error!("Attempted to convert invalid opcode to u8. This is a bug.");
|
error!("Attempted to convert invalid opcode to u8. This is a bug.");
|
||||||
8 // if this somehow happens, a close frame will help us tear down quickly
|
8 // if this somehow happens, a close frame will help us tear down quickly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,6 +212,7 @@ async fn h2_content_length() {
|
||||||
let value = HeaderValue::from_static("0");
|
let value = HeaderValue::from_static("0");
|
||||||
|
|
||||||
{
|
{
|
||||||
|
#[allow(clippy::single_element_loop)]
|
||||||
for &i in &[0] {
|
for &i in &[0] {
|
||||||
let req = srv
|
let req = srv
|
||||||
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
|
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
|
||||||
|
@ -226,6 +227,7 @@ async fn h2_content_length() {
|
||||||
// assert_eq!(response.headers().get(&header), None);
|
// assert_eq!(response.headers().get(&header), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::single_element_loop)]
|
||||||
for &i in &[1] {
|
for &i in &[1] {
|
||||||
let req = srv
|
let req = srv
|
||||||
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
||||||
|
|
|
@ -14,7 +14,7 @@ name = "actix_multipart"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4.0.0", default-features = false }
|
actix-web = { version = "4.0.0", default-features = false }
|
||||||
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
|
|
@ -23,16 +23,21 @@ default = ["http"]
|
||||||
bytestring = ">=0.1.5, <2"
|
bytestring = ">=0.1.5, <2"
|
||||||
firestorm = "0.5"
|
firestorm = "0.5"
|
||||||
http = { version = "0.2.3", optional = true }
|
http = { version = "0.2.3", optional = true }
|
||||||
log = "0.4"
|
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
firestorm = { version = "0.5", 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"] }
|
||||||
|
percent-encoding = "2.1"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "router"
|
name = "router"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "quoter"
|
||||||
|
harness = false
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
fn compare_quoters(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("Compare Quoters");
|
||||||
|
|
||||||
|
let quoter = actix_router::Quoter::new(b"", b"");
|
||||||
|
let path_quoted = (0..=0x7f)
|
||||||
|
.map(|c| format!("%{:02X}", c))
|
||||||
|
.collect::<String>();
|
||||||
|
let path_unquoted = ('\u{00}'..='\u{7f}').collect::<String>();
|
||||||
|
|
||||||
|
group.bench_function("quoter_unquoted", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
for _ in 0..10 {
|
||||||
|
black_box(quoter.requote(path_unquoted.as_bytes()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("percent_encode_unquoted", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
for _ in 0..10 {
|
||||||
|
let decode = percent_encoding::percent_decode(path_unquoted.as_bytes());
|
||||||
|
black_box(Into::<Cow<'_, [u8]>>::into(decode));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("quoter_quoted", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
for _ in 0..10 {
|
||||||
|
black_box(quoter.requote(path_quoted.as_bytes()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("percent_encode_quoted", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
for _ in 0..10 {
|
||||||
|
let decode = percent_encoding::percent_decode(path_quoted.as_bytes());
|
||||||
|
black_box(Into::<Cow<'_, [u8]>>::into(decode));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, compare_quoters);
|
||||||
|
criterion_main!(benches);
|
|
@ -7,7 +7,7 @@ use crate::path::{Path, PathIter};
|
||||||
use crate::{Quoter, ResourcePath};
|
use crate::{Quoter, ResourcePath};
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static FULL_QUOTER: Quoter = Quoter::new(b"+/%", b"");
|
static FULL_QUOTER: Quoter = Quoter::new(b"", b"");
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! unsupported_type {
|
macro_rules! unsupported_type {
|
||||||
|
|
|
@ -1,132 +1,89 @@
|
||||||
#[allow(dead_code)]
|
/// Partial percent-decoding.
|
||||||
const GEN_DELIMS: &[u8] = b":/?#[]@";
|
///
|
||||||
|
/// Performs percent-decoding on a slice but can selectively skip decoding certain sequences.
|
||||||
#[allow(dead_code)]
|
///
|
||||||
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
|
/// # Examples
|
||||||
|
/// ```
|
||||||
#[allow(dead_code)]
|
/// # use actix_router::Quoter;
|
||||||
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
|
/// // + is set as a protected character and will not be decoded...
|
||||||
|
/// let q = Quoter::new(&[], b"+");
|
||||||
#[allow(dead_code)]
|
///
|
||||||
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
|
/// // ...but the other encoded characters (like the hyphen below) will.
|
||||||
|
/// assert_eq!(q.requote(b"/a%2Db%2Bc").unwrap(), b"/a-b%2Bc");
|
||||||
#[allow(dead_code)]
|
/// ```
|
||||||
const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
|
||||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|
||||||
1234567890
|
|
||||||
-._~";
|
|
||||||
|
|
||||||
const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
|
||||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|
||||||
1234567890
|
|
||||||
-._~
|
|
||||||
!$'()*,";
|
|
||||||
|
|
||||||
const QS: &[u8] = b"+&=;b";
|
|
||||||
|
|
||||||
/// A quoter
|
|
||||||
pub struct Quoter {
|
pub struct Quoter {
|
||||||
/// Simple bit-map of safe values in the 0-127 ASCII range.
|
|
||||||
safe_table: [u8; 16],
|
|
||||||
|
|
||||||
/// Simple bit-map of protected values in the 0-127 ASCII range.
|
/// Simple bit-map of protected values in the 0-127 ASCII range.
|
||||||
protected_table: [u8; 16],
|
protected_table: AsciiBitmap,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Quoter {
|
impl Quoter {
|
||||||
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
|
/// Constructs a new `Quoter` instance given a set of protected ASCII bytes.
|
||||||
let mut quoter = Quoter {
|
///
|
||||||
safe_table: [0; 16],
|
/// The first argument is ignored but is kept for backward compatibility.
|
||||||
protected_table: [0; 16],
|
///
|
||||||
};
|
/// # Panics
|
||||||
|
/// Panics if any of the `protected` bytes are not in the 0-127 ASCII range.
|
||||||
// prepare safe table
|
pub fn new(_: &[u8], protected: &[u8]) -> Quoter {
|
||||||
for ch in 0..128 {
|
let mut protected_table = AsciiBitmap::default();
|
||||||
if ALLOWED.contains(&ch) {
|
|
||||||
set_bit(&mut quoter.safe_table, ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if QS.contains(&ch) {
|
|
||||||
set_bit(&mut quoter.safe_table, ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for &ch in safe {
|
|
||||||
set_bit(&mut quoter.safe_table, ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare protected table
|
// prepare protected table
|
||||||
for &ch in protected {
|
for &ch in protected {
|
||||||
set_bit(&mut quoter.safe_table, ch);
|
protected_table.set_bit(ch);
|
||||||
set_bit(&mut quoter.protected_table, ch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
quoter
|
Quoter { protected_table }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes safe percent-encoded sequences from `val`.
|
/// Decodes the next escape sequence, if any, and advances `val`.
|
||||||
|
#[inline(always)]
|
||||||
|
fn decode_next<'a>(&self, val: &mut &'a [u8]) -> Option<(&'a [u8], u8)> {
|
||||||
|
for i in 0..val.len() {
|
||||||
|
if let (prev, [b'%', p1, p2, rem @ ..]) = val.split_at(i) {
|
||||||
|
if let Some(ch) = hex_pair_to_char(*p1, *p2)
|
||||||
|
// ignore protected ascii bytes
|
||||||
|
.filter(|&ch| !(ch < 128 && self.protected_table.bit_at(ch)))
|
||||||
|
{
|
||||||
|
*val = rem;
|
||||||
|
return Some((prev, ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Partially percent-decodes the given bytes.
|
||||||
///
|
///
|
||||||
/// Returns `None` when no modification to the original byte string was required.
|
/// Escape sequences of the protected set are *not* decoded.
|
||||||
///
|
///
|
||||||
/// Non-ASCII bytes are accepted as valid input.
|
/// Returns `None` when no modification to the original bytes was required.
|
||||||
///
|
///
|
||||||
/// Behavior for invalid/incomplete percent-encoding sequences is unspecified and may include
|
/// Invalid/incomplete percent-encoding sequences are passed unmodified.
|
||||||
/// removing the invalid sequence from the output or passing it as-is.
|
|
||||||
pub fn requote(&self, val: &[u8]) -> Option<Vec<u8>> {
|
pub fn requote(&self, val: &[u8]) -> Option<Vec<u8>> {
|
||||||
let mut has_pct = 0;
|
let mut remaining = val;
|
||||||
let mut pct = [b'%', 0, 0];
|
|
||||||
let mut idx = 0;
|
|
||||||
let mut cloned: Option<Vec<u8>> = None;
|
|
||||||
|
|
||||||
let len = val.len();
|
// early return indicates that no percent-encoded sequences exist and we can skip allocation
|
||||||
|
let (pre, decoded_char) = self.decode_next(&mut remaining)?;
|
||||||
|
|
||||||
while idx < len {
|
// decoded output will always be shorter than the input
|
||||||
let ch = val[idx];
|
let mut decoded = Vec::<u8>::with_capacity(val.len());
|
||||||
|
|
||||||
if has_pct != 0 {
|
// push first segment and decoded char
|
||||||
pct[has_pct] = val[idx];
|
decoded.extend_from_slice(pre);
|
||||||
has_pct += 1;
|
decoded.push(decoded_char);
|
||||||
|
|
||||||
if has_pct == 3 {
|
// decode and push rest of segments and decoded chars
|
||||||
has_pct = 0;
|
while let Some((prev, ch)) = self.decode_next(&mut remaining) {
|
||||||
let buf = cloned.as_mut().unwrap();
|
// this ugly conditional achieves +50% perf in cases where this is a tight loop.
|
||||||
|
if !prev.is_empty() {
|
||||||
if let Some(ch) = hex_pair_to_char(pct[1], pct[2]) {
|
decoded.extend_from_slice(prev);
|
||||||
if ch < 128 {
|
}
|
||||||
if bit_at(&self.protected_table, ch) {
|
decoded.push(ch);
|
||||||
buf.extend_from_slice(&pct);
|
|
||||||
idx += 1;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if bit_at(&self.safe_table, ch) {
|
decoded.extend_from_slice(remaining);
|
||||||
buf.push(ch);
|
|
||||||
idx += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.push(ch);
|
Some(decoded)
|
||||||
} else {
|
|
||||||
buf.extend_from_slice(&pct[..]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ch == b'%' {
|
|
||||||
has_pct = 1;
|
|
||||||
|
|
||||||
if cloned.is_none() {
|
|
||||||
let mut c = Vec::with_capacity(len);
|
|
||||||
c.extend_from_slice(&val[..idx]);
|
|
||||||
cloned = Some(c);
|
|
||||||
}
|
|
||||||
} else if let Some(ref mut cloned) = cloned {
|
|
||||||
cloned.push(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
idx += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn requote_str_lossy(&self, val: &str) -> Option<String> {
|
pub(crate) fn requote_str_lossy(&self, val: &str) -> Option<String> {
|
||||||
|
@ -135,24 +92,6 @@ impl Quoter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts an ASCII character in the hex-encoded set (`0-9`, `A-F`, `a-f`) to its integer
|
|
||||||
/// representation from `0x0`–`0xF`.
|
|
||||||
///
|
|
||||||
/// - `0x30 ('0') => 0x0`
|
|
||||||
/// - `0x39 ('9') => 0x9`
|
|
||||||
/// - `0x41 ('a') => 0xA`
|
|
||||||
/// - `0x61 ('A') => 0xA`
|
|
||||||
/// - `0x46 ('f') => 0xF`
|
|
||||||
/// - `0x66 ('F') => 0xF`
|
|
||||||
fn from_ascii_hex(v: u8) -> Option<u8> {
|
|
||||||
match v {
|
|
||||||
b'0'..=b'9' => Some(v - 0x30), // ord('0') == 0x30
|
|
||||||
b'A'..=b'F' => Some(v - 0x41 + 10), // ord('A') == 0x41
|
|
||||||
b'a'..=b'f' => Some(v - 0x61 + 10), // ord('a') == 0x61
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode a ASCII hex-encoded pair to an integer.
|
/// Decode a ASCII hex-encoded pair to an integer.
|
||||||
///
|
///
|
||||||
/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value.
|
/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value.
|
||||||
|
@ -160,64 +99,52 @@ fn from_ascii_hex(v: u8) -> Option<u8> {
|
||||||
/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')`
|
/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')`
|
||||||
/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')`
|
/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')`
|
||||||
/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')`
|
/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')`
|
||||||
|
#[inline(always)]
|
||||||
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
|
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
|
||||||
let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?);
|
let d_high = char::from(d1).to_digit(16)?;
|
||||||
|
let d_low = char::from(d2).to_digit(16)?;
|
||||||
|
|
||||||
// left shift high nibble by 4 bits
|
// left shift high nibble by 4 bits
|
||||||
Some(d_high << 4 | d_low)
|
Some((d_high as u8) << 4 | (d_low as u8))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
struct AsciiBitmap {
|
||||||
|
array: [u8; 16],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsciiBitmap {
|
||||||
/// Sets bit in given bit-map to 1=true.
|
/// Sets bit in given bit-map to 1=true.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if `ch` index is out of bounds.
|
/// Panics if `ch` index is out of bounds.
|
||||||
fn set_bit(array: &mut [u8], ch: u8) {
|
fn set_bit(&mut self, ch: u8) {
|
||||||
array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
|
self.array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if bit to true in given bit-map.
|
/// Returns true if bit to true in given bit-map.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if `ch` index is out of bounds.
|
/// Panics if `ch` index is out of bounds.
|
||||||
fn bit_at(array: &[u8], ch: u8) -> bool {
|
fn bit_at(&self, ch: u8) -> bool {
|
||||||
array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
|
self.array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hex_encoding() {
|
|
||||||
let hex = b"0123456789abcdefABCDEF";
|
|
||||||
|
|
||||||
for i in 0..256 {
|
|
||||||
let c = i as u8;
|
|
||||||
if hex.contains(&c) {
|
|
||||||
assert!(from_ascii_hex(c).is_some())
|
|
||||||
} else {
|
|
||||||
assert!(from_ascii_hex(c).is_none())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let expected = [
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15,
|
|
||||||
];
|
|
||||||
for i in 0..hex.len() {
|
|
||||||
assert_eq!(from_ascii_hex(hex[i]).unwrap(), expected[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_quoter() {
|
fn custom_quoter() {
|
||||||
let q = Quoter::new(b"", b"+");
|
let q = Quoter::new(b"", b"+");
|
||||||
assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
|
assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
|
||||||
assert_eq!(q.requote(b"/a%2Bc").unwrap(), b"/a%2Bc");
|
assert_eq!(q.requote(b"/a%2Bc"), None);
|
||||||
|
|
||||||
let q = Quoter::new(b"%+", b"/");
|
let q = Quoter::new(b"%+", b"/");
|
||||||
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
|
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
|
||||||
assert_eq!(q.requote(b"/a%2fb").unwrap(), b"/a%2fb");
|
assert_eq!(q.requote(b"/a%2fb"), None);
|
||||||
assert_eq!(q.requote(b"/a%2Fb").unwrap(), b"/a%2Fb");
|
assert_eq!(q.requote(b"/a%2Fb"), None);
|
||||||
assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb");
|
assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb");
|
||||||
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
||||||
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
||||||
|
@ -233,7 +160,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_sequences() {
|
fn invalid_sequences() {
|
||||||
let q = Quoter::new(b"%+", b"/");
|
let q = Quoter::new(b"%+", b"/");
|
||||||
assert_eq!(q.requote(b"/a%2x%2X%%").unwrap(), b"/a%2x%2X");
|
assert_eq!(q.requote(b"/a%2x%2X%%"), None);
|
||||||
|
assert_eq!(q.requote(b"/a%20%2X%%").unwrap(), b"/a %2X%%");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::{
|
||||||
|
|
||||||
use firestorm::{profile_fn, profile_method, profile_section};
|
use firestorm::{profile_fn, profile_method, profile_section};
|
||||||
use regex::{escape, Regex, RegexSet};
|
use regex::{escape, Regex, RegexSet};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
|
use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
|
||||||
|
|
||||||
|
@ -714,10 +715,7 @@ impl ResourceDef {
|
||||||
if let Some(m) = captures.name(name) {
|
if let Some(m) = captures.name(name) {
|
||||||
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
||||||
} else {
|
} else {
|
||||||
log::error!(
|
error!("Dynamic path match but not all segments found: {}", name);
|
||||||
"Dynamic path match but not all segments found: {}",
|
|
||||||
name
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -744,7 +742,7 @@ impl ResourceDef {
|
||||||
if let Some(m) = captures.name(name) {
|
if let Some(m) = captures.name(name) {
|
||||||
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
||||||
} else {
|
} else {
|
||||||
log::error!("Dynamic path match but not all segments found: {}", name);
|
error!("Dynamic path match but not all segments found: {}", name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1038,7 +1036,7 @@ impl ResourceDef {
|
||||||
// tail segments in prefixes have no defined semantics
|
// tail segments in prefixes have no defined semantics
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
log::warn!(
|
tracing::warn!(
|
||||||
"Prefix resources should not have tail segments. \
|
"Prefix resources should not have tail segments. \
|
||||||
Use `ResourceDef::new` constructor. \
|
Use `ResourceDef::new` constructor. \
|
||||||
This may become a panic in the future."
|
This may become a panic in the future."
|
||||||
|
@ -1053,7 +1051,7 @@ impl ResourceDef {
|
||||||
// unnamed tail segment
|
// unnamed tail segment
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
log::warn!(
|
tracing::warn!(
|
||||||
"Tail segments must have names. \
|
"Tail segments must have names. \
|
||||||
Consider `.../{{tail}}*`. \
|
Consider `.../{{tail}}*`. \
|
||||||
This may become a panic in the future."
|
This may become a panic in the future."
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::ResourcePath;
|
||||||
use crate::Quoter;
|
use crate::Quoter;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
|
static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
|
|
|
@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-http = "3.0.0"
|
actix-http = "3"
|
||||||
actix-http-test = "3.0.0-beta.13"
|
actix-http-test = "3.0.0-beta.13"
|
||||||
actix-rt = "2.1"
|
actix-rt = "2.1"
|
||||||
actix-service = "2.0.0"
|
actix-service = "2"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||||
awc = { version = "3.0.0-beta.21", default-features = false, features = ["cookies"] }
|
awc = { version = "3", 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 = [] }
|
||||||
|
|
|
@ -3,6 +3,12 @@
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 4.1.0 - 2022-03-02
|
||||||
|
- Add support for `actix` version `0.13`. [#2675]
|
||||||
|
|
||||||
|
[#2675]: https://github.com/actix/actix-web/pull/2675
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0 - 2022-02-25
|
## 4.0.0 - 2022-02-25
|
||||||
- No significant changes since `4.0.0-beta.12`.
|
- No significant changes since `4.0.0-beta.12`.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-web-actors"
|
name = "actix-web-actors"
|
||||||
version = "4.0.0"
|
version = "4.1.0"
|
||||||
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"]
|
||||||
|
@ -14,21 +14,21 @@ name = "actix_web_actors"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = { version = "0.12.0", default-features = false }
|
actix = { version = ">=0.12, <0.14", default-features = false }
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-http = "3.0.0"
|
actix-http = "3"
|
||||||
actix-web = { version = "4.0.0", default-features = false }
|
actix-web = { version = "4", 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.8.4", features = ["sync"] }
|
tokio = { version = "1.13.1", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-test = "0.1.0-beta.13"
|
actix-test = "0.1.0-beta.13"
|
||||||
awc = { version = "3.0.0-beta.21", default-features = false }
|
awc = { version = "3", 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)
|
[](https://docs.rs/actix-web-actors/4.1.0)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-web-actors/4.0.0)
|
[](https://deps.rs/crate/actix-web-actors/4.1.0)
|
||||||
[](https://crates.io/crates/actix-web-actors)
|
[](https://crates.io/crates/actix-web-actors)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
- Updated `cookie` to `0.16`. [#2555]
|
- Updated `cookie` to `0.16`. [#2555]
|
||||||
- Updated `language-tags` to `0.3`.
|
- Updated `language-tags` to `0.3`.
|
||||||
- Updated `rand` to `0.8`.
|
- Updated `rand` to `0.8`.
|
||||||
- Updated `rustls` to `0.20.0`. [#2414]
|
- Updated `rustls` to `0.20`. [#2414]
|
||||||
- Updated `tokio` to `1`.
|
- Updated `tokio` to `1`.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -71,9 +71,9 @@ actix-service = "2"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-tls = { version = "3", default-features = false, optional = true }
|
actix-tls = { version = "3", default-features = false, optional = true }
|
||||||
|
|
||||||
actix-http = { version = "3.0.0", features = ["http2", "ws"] }
|
actix-http = { version = "3", features = ["http2", "ws"] }
|
||||||
actix-router = "0.5.0"
|
actix-router = "0.5"
|
||||||
actix-web-codegen = { version = "4.0.0", optional = true }
|
actix-web-codegen = { version = "4", optional = true }
|
||||||
|
|
||||||
ahash = "0.7"
|
ahash = "0.7"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
@ -90,7 +90,7 @@ once_cell = "1.5"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
pin-project-lite = "0.2.7"
|
pin-project-lite = "0.2.7"
|
||||||
regex = "1.4"
|
regex = "1.5.5"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
|
@ -100,9 +100,9 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-files = "0.6.0"
|
actix-files = "0.6"
|
||||||
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
|
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
|
||||||
awc = { version = "3.0.0-beta.21", features = ["openssl"] }
|
awc = { version = "3", features = ["openssl"] }
|
||||||
|
|
||||||
brotli = "3.3.3"
|
brotli = "3.3.3"
|
||||||
const-str = "0.3"
|
const-str = "0.3"
|
||||||
|
@ -118,7 +118,7 @@ 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.13.1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
|
||||||
zstd = "0.10"
|
zstd = "0.11"
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "test_server"
|
name = "test_server"
|
||||||
|
|
|
@ -29,8 +29,9 @@ Headings marked with :warning: are **breaking behavioral changes**. They will pr
|
||||||
- [Server Must Be Polled :warning:](#server-must-be-polled-warning)
|
- [Server Must Be Polled :warning:](#server-must-be-polled-warning)
|
||||||
- [Guards API](#guards-api)
|
- [Guards API](#guards-api)
|
||||||
- [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously)
|
- [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously)
|
||||||
- [`#[actix_web::main]` and `#[tokio::main]`](#actixwebmain-and-tokiomain)
|
- [`#[actix_web::main]` and `#[tokio::main]`](#actix_webmain-and-tokiomain)
|
||||||
- [`web::block`](#webblock)
|
- [`web::block`](#webblock)
|
||||||
|
-
|
||||||
|
|
||||||
## MSRV
|
## MSRV
|
||||||
|
|
||||||
|
@ -483,3 +484,24 @@ The `web::block` helper has changed return type from roughly `async fn(fn() -> R
|
||||||
- let n: u32 = web::block(|| Ok(123)).await?;
|
- let n: u32 = web::block(|| Ok(123)).await?;
|
||||||
+ let n: u32 = web::block(|| Ok(123)).await??;
|
+ let n: u32 = web::block(|| Ok(123)).await??;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `HttpResponse` as a `ResponseError`
|
||||||
|
|
||||||
|
The implementation of `ResponseError` for `HttpResponse` has been removed.
|
||||||
|
|
||||||
|
It was common in v3 to use `HttpResponse` as an error type in fallible handlers. The problem is that `HttpResponse` contains no knowledge or reference to the source error. Being able to guarantee that an "error" response actually contains an error reference makes middleware and other parts of Actix Web more effective.
|
||||||
|
|
||||||
|
The error response builders in the `error` module were available in v3 but are now the best method for simple error responses without requiring you to implement the trait on your own custom error types. These builders can receive simple strings and third party errors that can not implement the `ResponseError` trait.
|
||||||
|
|
||||||
|
A few common patterns are affected by this change:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- Err(HttpResponse::InternalServerError().finish())
|
||||||
|
+ Err(error::ErrorInternalServerError("reason"))
|
||||||
|
|
||||||
|
- Err(HttpResponse::InternalServerError().body(third_party_error.to_string()))
|
||||||
|
+ Err(error::ErrorInternalServerError(err))
|
||||||
|
|
||||||
|
- .map_err(|err| HttpResponse::InternalServerError().finish())?
|
||||||
|
+ .map_err(error::ErrorInternalServerError)?
|
||||||
|
```
|
||||||
|
|
|
@ -56,15 +56,16 @@ Code:
|
||||||
```rust
|
```rust
|
||||||
use actix_web::{get, web, App, HttpServer, Responder};
|
use actix_web::{get, web, App, HttpServer, Responder};
|
||||||
|
|
||||||
#[get("/{id}/{name}/index.html")]
|
#[get("/hello/{name}")]
|
||||||
async fn index(params: web::Path<(u32, String)>) -> impl Responder {
|
async fn greet(name: web::Path<String>) -> impl Responder {
|
||||||
let (id, name) = params.into_inner();
|
format!("Hello {name}!")
|
||||||
format!("Hello {}! id:{}", name, id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main] // or #[tokio::main]
|
#[actix_web::main] // or #[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
HttpServer::new(|| App::new().service(index))
|
HttpServer::new(|| {
|
||||||
|
App::new().service(greet)
|
||||||
|
})
|
||||||
.bind(("127.0.0.1", 8080))?
|
.bind(("127.0.0.1", 8080))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -5,10 +5,7 @@ use actix_utils::future::{err, ok, Ready};
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{dev::Payload, error, Error, FromRequest, HttpRequest};
|
||||||
dev::Payload, error::ErrorInternalServerError, extract::FromRequest, request::HttpRequest,
|
|
||||||
Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Data factory.
|
/// Data factory.
|
||||||
pub(crate) trait DataFactory {
|
pub(crate) trait DataFactory {
|
||||||
|
@ -160,7 +157,7 @@ impl<T: ?Sized + 'static> FromRequest for Data<T> {
|
||||||
req.match_name().unwrap_or_else(|| req.path())
|
req.match_name().unwrap_or_else(|| req.path())
|
||||||
);
|
);
|
||||||
|
|
||||||
err(ErrorInternalServerError(
|
err(error::ErrorInternalServerError(
|
||||||
"Requested application data is not configured correctly. \
|
"Requested application data is not configured correctly. \
|
||||||
View/enable debug logs for more details.",
|
View/enable debug logs for more details.",
|
||||||
))
|
))
|
||||||
|
|
|
@ -159,7 +159,7 @@ impl ConnectionInfo {
|
||||||
pub fn realip_remote_addr(&self) -> Option<&str> {
|
pub fn realip_remote_addr(&self) -> Option<&str> {
|
||||||
self.realip_remote_addr
|
self.realip_remote_addr
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.or_else(|| self.peer_addr.as_deref())
|
.or(self.peer_addr.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns serialized IP address of the peer connection.
|
/// Returns serialized IP address of the peer connection.
|
||||||
|
|
|
@ -4,16 +4,17 @@
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use actix_web::{get, web, App, HttpServer, Responder};
|
//! use actix_web::{get, web, App, HttpServer, Responder};
|
||||||
//!
|
//!
|
||||||
//! #[get("/{id}/{name}/index.html")]
|
//! #[get("/hello/{name}")]
|
||||||
//! async fn index(path: web::Path<(u32, String)>) -> impl Responder {
|
//! async fn greet(name: web::Path<String>) -> impl Responder {
|
||||||
//! let (id, name) = path.into_inner();
|
//! format!("Hello {}!", name)
|
||||||
//! format!("Hello {}! id:{}", name, id)
|
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! #[actix_web::main]
|
//! #[actix_web::main] // or #[tokio::main]
|
||||||
//! async fn main() -> std::io::Result<()> {
|
//! async fn main() -> std::io::Result<()> {
|
||||||
//! HttpServer::new(|| App::new().service(index))
|
//! HttpServer::new(|| {
|
||||||
//! .bind("127.0.0.1:8080")?
|
//! App::new().service(greet)
|
||||||
|
//! })
|
||||||
|
//! .bind(("127.0.0.1", 8080))?
|
||||||
//! .run()
|
//! .run()
|
||||||
//! .await
|
//! .await
|
||||||
//! }
|
//! }
|
||||||
|
|
|
@ -11,3 +11,6 @@
|
||||||
## Error Propagation
|
## Error Propagation
|
||||||
|
|
||||||
## When To (Not) Use Middleware
|
## When To (Not) Use Middleware
|
||||||
|
|
||||||
|
## Author's References
|
||||||
|
- `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428
|
||||||
|
|
|
@ -151,7 +151,7 @@ impl ResourceMap {
|
||||||
.char_indices()
|
.char_indices()
|
||||||
.filter_map(|(i, c)| (c == '/').then(|| i))
|
.filter_map(|(i, c)| (c == '/').then(|| i))
|
||||||
.nth(2)
|
.nth(2)
|
||||||
.unwrap_or_else(|| path.len());
|
.unwrap_or(path.len());
|
||||||
|
|
||||||
(
|
(
|
||||||
Cow::Borrowed(&path[..third_slash_index]),
|
Cow::Borrowed(&path[..third_slash_index]),
|
||||||
|
|
|
@ -367,9 +367,7 @@ where
|
||||||
.local_addr(addr);
|
.local_addr(addr);
|
||||||
|
|
||||||
let svc = if let Some(handler) = on_connect_fn.clone() {
|
let svc = if let Some(handler) = on_connect_fn.clone() {
|
||||||
svc.on_connect_ext(move |io: &_, ext: _| {
|
svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext))
|
||||||
(&*handler)(io as &dyn Any, ext)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
svc
|
svc
|
||||||
};
|
};
|
||||||
|
@ -555,7 +553,7 @@ where
|
||||||
|
|
||||||
if let Some(handler) = on_connect_fn.clone() {
|
if let Some(handler) = on_connect_fn.clone() {
|
||||||
svc = svc
|
svc = svc
|
||||||
.on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext));
|
.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext));
|
||||||
}
|
}
|
||||||
|
|
||||||
let fac = factory()
|
let fac = factory()
|
||||||
|
|
|
@ -78,18 +78,18 @@ pub struct ServiceRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceRequest {
|
impl ServiceRequest {
|
||||||
/// Construct service request
|
/// Construct `ServiceRequest` from parts.
|
||||||
pub(crate) fn new(req: HttpRequest, payload: Payload) -> Self {
|
pub(crate) fn new(req: HttpRequest, payload: Payload) -> Self {
|
||||||
Self { req, payload }
|
Self { req, payload }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deconstruct request into parts
|
/// Deconstruct `ServiceRequest` into inner parts.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn into_parts(self) -> (HttpRequest, Payload) {
|
pub fn into_parts(self) -> (HttpRequest, Payload) {
|
||||||
(self.req, self.payload)
|
(self.req, self.payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get mutable access to inner `HttpRequest` and `Payload`
|
/// Returns mutable accessors to inner parts.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) {
|
pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) {
|
||||||
(&mut self.req, &mut self.payload)
|
(&mut self.req, &mut self.payload)
|
||||||
|
@ -130,9 +130,7 @@ impl ServiceRequest {
|
||||||
Self { req, payload }
|
Self { req, payload }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct request from request.
|
/// Construct `ServiceRequest` with no payload from given `HttpRequest`.
|
||||||
///
|
|
||||||
/// The returned `ServiceRequest` would have no payload.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_request(req: HttpRequest) -> Self {
|
pub fn from_request(req: HttpRequest) -> Self {
|
||||||
ServiceRequest {
|
ServiceRequest {
|
||||||
|
@ -141,63 +139,63 @@ impl ServiceRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create service response
|
/// Create `ServiceResponse` from this request and given response.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn into_response<B, R: Into<Response<B>>>(self, res: R) -> ServiceResponse<B> {
|
pub fn into_response<B, R: Into<Response<B>>>(self, res: R) -> ServiceResponse<B> {
|
||||||
let res = HttpResponse::from(res.into());
|
let res = HttpResponse::from(res.into());
|
||||||
ServiceResponse::new(self.req, res)
|
ServiceResponse::new(self.req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create service response for error
|
/// Create `ServiceResponse` from this request and given error.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn error_response<E: Into<Error>>(self, err: E) -> ServiceResponse {
|
pub fn error_response<E: Into<Error>>(self, err: E) -> ServiceResponse {
|
||||||
let res = HttpResponse::from_error(err.into());
|
let res = HttpResponse::from_error(err.into());
|
||||||
ServiceResponse::new(self.req, res)
|
ServiceResponse::new(self.req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This method returns reference to the request head
|
/// Returns a reference to the request head.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn head(&self) -> &RequestHead {
|
pub fn head(&self) -> &RequestHead {
|
||||||
self.req.head()
|
self.req.head()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This method returns reference to the request head
|
/// Returns a mutable reference to the request head.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn head_mut(&mut self) -> &mut RequestHead {
|
pub fn head_mut(&mut self) -> &mut RequestHead {
|
||||||
self.req.head_mut()
|
self.req.head_mut()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request's uri.
|
/// Returns the request URI.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn uri(&self) -> &Uri {
|
pub fn uri(&self) -> &Uri {
|
||||||
&self.head().uri
|
&self.head().uri
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the Request method.
|
/// Returns the request method.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn method(&self) -> &Method {
|
pub fn method(&self) -> &Method {
|
||||||
&self.head().method
|
&self.head().method
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the Request Version.
|
/// Returns the request version.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn version(&self) -> Version {
|
pub fn version(&self) -> Version {
|
||||||
self.head().version
|
self.head().version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to request headers.
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Returns request's headers.
|
|
||||||
pub fn headers(&self) -> &HeaderMap {
|
pub fn headers(&self) -> &HeaderMap {
|
||||||
&self.head().headers
|
&self.head().headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to request headers.
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Returns mutable request's headers.
|
|
||||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||||
&mut self.head_mut().headers
|
&mut self.head_mut().headers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The target path of this Request.
|
/// Returns request path.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn path(&self) -> &str {
|
pub fn path(&self) -> &str {
|
||||||
self.head().uri.path()
|
self.head().uri.path()
|
||||||
|
@ -209,7 +207,7 @@ impl ServiceRequest {
|
||||||
self.req.query_string()
|
self.req.query_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peer socket address.
|
/// Returns peer's 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.
|
||||||
|
@ -222,24 +220,23 @@ impl ServiceRequest {
|
||||||
self.head().peer_addr
|
self.head().peer_addr
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get *ConnectionInfo* for the current request.
|
/// Returns a reference to connection info.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
|
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
|
||||||
self.req.connection_info()
|
self.req.connection_info()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the Path parameters.
|
/// Returns reference to the Path parameters.
|
||||||
///
|
///
|
||||||
/// Params is a container for URL parameters.
|
/// Params is a container for URL parameters. A variable segment is specified in the form
|
||||||
/// A variable segment is specified in the form `{identifier}`,
|
/// `{identifier}`, where the identifier can be used later in a request handler to access the
|
||||||
/// where the identifier can be used later in a request handler to
|
/// matched value for that segment.
|
||||||
/// access the matched value for that segment.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn match_info(&self) -> &Path<Url> {
|
pub fn match_info(&self) -> &Path<Url> {
|
||||||
self.req.match_info()
|
self.req.match_info()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a mutable reference to the Path parameters.
|
/// Returns a mutable reference to the path match information.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn match_info_mut(&mut self) -> &mut Path<Url> {
|
pub fn match_info_mut(&mut self) -> &mut Path<Url> {
|
||||||
self.req.match_info_mut()
|
self.req.match_info_mut()
|
||||||
|
@ -257,13 +254,13 @@ impl ServiceRequest {
|
||||||
self.req.match_pattern()
|
self.req.match_pattern()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to a `ResourceMap` of current application.
|
/// Returns a reference to the application's resource map.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn resource_map(&self) -> &ResourceMap {
|
pub fn resource_map(&self) -> &ResourceMap {
|
||||||
self.req.resource_map()
|
self.req.resource_map()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Service configuration
|
/// Returns a reference to the application's configuration.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn app_config(&self) -> &AppConfig {
|
pub fn app_config(&self) -> &AppConfig {
|
||||||
self.req.app_config()
|
self.req.app_config()
|
||||||
|
@ -287,6 +284,7 @@ impl ServiceRequest {
|
||||||
self.req.conn_data()
|
self.req.conn_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return request cookies.
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
||||||
|
|
|
@ -24,16 +24,16 @@ use crate::cookie::{Cookie, CookieJar};
|
||||||
///
|
///
|
||||||
/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern.
|
/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern.
|
||||||
/// You can generate various types of request via TestRequest's methods:
|
/// You can generate various types of request via TestRequest's methods:
|
||||||
/// * `TestRequest::to_request` creates `actix_http::Request` instance.
|
/// - [`TestRequest::to_request`] creates an [`actix_http::Request`](Request).
|
||||||
/// * `TestRequest::to_srv_request` creates `ServiceRequest` instance, which is used for testing middlewares and chain adapters.
|
/// - [`TestRequest::to_srv_request`] creates a [`ServiceRequest`], which is used for testing middlewares and chain adapters.
|
||||||
/// * `TestRequest::to_srv_response` creates `ServiceResponse` instance.
|
/// - [`TestRequest::to_srv_response`] creates a [`ServiceResponse`].
|
||||||
/// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers.
|
/// - [`TestRequest::to_http_request`] creates an [`HttpRequest`], which is used for testing handlers.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage};
|
/// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage};
|
||||||
/// use actix_web::http::{header, StatusCode};
|
/// use actix_web::http::{header, StatusCode};
|
||||||
///
|
///
|
||||||
/// async fn index(req: HttpRequest) -> HttpResponse {
|
/// async fn handler(req: HttpRequest) -> HttpResponse {
|
||||||
/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
|
/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
|
||||||
/// HttpResponse::Ok().into()
|
/// HttpResponse::Ok().into()
|
||||||
/// } else {
|
/// } else {
|
||||||
|
@ -42,15 +42,18 @@ use crate::cookie::{Cookie, CookieJar};
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[actix_web::test]
|
/// #[actix_web::test]
|
||||||
|
/// # // force rustdoc to display the correct thing and also compile check the test
|
||||||
|
/// # async fn _test() {}
|
||||||
/// async fn test_index() {
|
/// async fn test_index() {
|
||||||
/// let req = test::TestRequest::default().insert_header("content-type", "text/plain")
|
/// let req = test::TestRequest::default()
|
||||||
|
/// .insert_header(header::ContentType::plaintext())
|
||||||
/// .to_http_request();
|
/// .to_http_request();
|
||||||
///
|
///
|
||||||
/// let resp = index(req).await.unwrap();
|
/// let resp = handler(req).await;
|
||||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||||
///
|
///
|
||||||
/// let req = test::TestRequest::default().to_http_request();
|
/// let req = test::TestRequest::default().to_http_request();
|
||||||
/// let resp = index(req).await.unwrap();
|
/// let resp = index(req).await;
|
||||||
/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
@ -3,6 +3,103 @@
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0 - 2022-03-07
|
||||||
|
### Dependencies
|
||||||
|
- Updated `actix-*` to Tokio v1-based versions. [#1813]
|
||||||
|
- Updated `bytes` to `1.0`. [#1813]
|
||||||
|
- Updated `cookie` to `0.16`. [#2555]
|
||||||
|
- Updated `rand` to `0.8`.
|
||||||
|
- Updated `rustls` to `0.20`. [#2414]
|
||||||
|
- Updated `tokio` to `1`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `trust-dns` crate feature to enable `trust-dns-resolver` as client DNS resolver; disabled by default. [#1969]
|
||||||
|
- `cookies` crate feature; enabled by default. [#2619]
|
||||||
|
- `compress-brotli` crate feature; enabled by default. [#2250]
|
||||||
|
- `compress-gzip` crate feature; enabled by default. [#2250]
|
||||||
|
- `compress-zstd` crate feature; enabled by default. [#2250]
|
||||||
|
- `client::Connector::handshake_timeout()` for customizing TLS connection handshake timeout. [#2081]
|
||||||
|
- `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
|
||||||
|
- `client::ConnectionIo` trait alias [#2081]
|
||||||
|
- `Client::headers()` to get default mut reference of `HeaderMap` of client object. [#2114]
|
||||||
|
- `ClientResponse::timeout()` for set the timeout of collecting response body. [#1931]
|
||||||
|
- `ClientBuilder::local_address()` for binding to a local IP address for this client. [#2024]
|
||||||
|
- `ClientRequest::insert_header()` method which allows using typed and untyped headers. [#1869]
|
||||||
|
- `ClientRequest::append_header()` method which allows using typed and untyped headers. [#1869]
|
||||||
|
- `ClientBuilder::add_default_header()` (and deprecate `ClientBuilder::header()`). [#2510]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `client::Connector` type now only has one generic type for `actix_service::Service`. [#2063]
|
||||||
|
- `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type. [#1905]
|
||||||
|
- `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905]
|
||||||
|
- `ConnectorService` type is renamed to `BoxConnectorService`. [#2081]
|
||||||
|
- Fix http/https encoding when enabling `compress` feature. [#2116]
|
||||||
|
- Rename `TestResponse::{header => append_header, set => insert_header}`. These methods now take a `TryIntoHeaderPair`. [#2094]
|
||||||
|
- `ClientBuilder::connector()` method now takes `Connector<T, U>` type. [#2008]
|
||||||
|
- Basic auth now accepts blank passwords as an empty string instead of an `Option`. [#2050]
|
||||||
|
- Relax default timeout for `Connector` to 5 seconds (up from 1 second). [#1905]
|
||||||
|
- `*::send_json()` and `*::send_form()` methods now receive `impl Serialize`. [#2553]
|
||||||
|
- `FrozenClientRequest::extra_header()` now uses receives an `impl TryIntoHeaderPair`. [#2553]
|
||||||
|
- Rename `Connector::{ssl => openssl}()`. [#2503]
|
||||||
|
- `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546]
|
||||||
|
- Rename `MessageBody => ResponseBody` to avoid conflicts with `MessageBody` trait. [#2546]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Send headers along with redirected requests. [#2310]
|
||||||
|
- Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503]
|
||||||
|
- Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553]
|
||||||
|
- `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]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `compress` crate feature. [#2250]
|
||||||
|
- `ClientRequest::set`; use `ClientRequest::insert_header`. [#1869]
|
||||||
|
- `ClientRequest::set_header`; use `ClientRequest::insert_header`. [#1869]
|
||||||
|
- `ClientRequest::set_header_if_none`; use `ClientRequest::insert_header_if_none`. [#1869]
|
||||||
|
- `ClientRequest::header`; use `ClientRequest::append_header`. [#1869]
|
||||||
|
- Deprecated methods on `ClientRequest`: `if_true`, `if_some`. [#2148]
|
||||||
|
- `ClientBuilder::default` function [#2008]
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- `cookie` upgrade addresses [`RUSTSEC-2020-0071`].
|
||||||
|
|
||||||
|
[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html
|
||||||
|
|
||||||
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
[#1869]: https://github.com/actix/actix-web/pull/1869
|
||||||
|
[#1905]: https://github.com/actix/actix-web/pull/1905
|
||||||
|
[#1905]: https://github.com/actix/actix-web/pull/1905
|
||||||
|
[#1931]: https://github.com/actix/actix-web/pull/1931
|
||||||
|
[#1969]: https://github.com/actix/actix-web/pull/1969
|
||||||
|
[#1969]: https://github.com/actix/actix-web/pull/1969
|
||||||
|
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||||
|
[#2008]: https://github.com/actix/actix-web/pull/2008
|
||||||
|
[#2024]: https://github.com/actix/actix-web/pull/2024
|
||||||
|
[#2050]: https://github.com/actix/actix-web/pull/2050
|
||||||
|
[#2063]: https://github.com/actix/actix-web/pull/2063
|
||||||
|
[#2081]: https://github.com/actix/actix-web/pull/2081
|
||||||
|
[#2081]: https://github.com/actix/actix-web/pull/2081
|
||||||
|
[#2094]: https://github.com/actix/actix-web/pull/2094
|
||||||
|
[#2114]: https://github.com/actix/actix-web/pull/2114
|
||||||
|
[#2116]: https://github.com/actix/actix-web/pull/2116
|
||||||
|
[#2148]: https://github.com/actix/actix-web/pull/2148
|
||||||
|
[#2250]: https://github.com/actix/actix-web/pull/2250
|
||||||
|
[#2310]: https://github.com/actix/actix-web/pull/2310
|
||||||
|
[#2414]: https://github.com/actix/actix-web/pull/2414
|
||||||
|
[#2425]: https://github.com/actix/actix-web/pull/2425
|
||||||
|
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||||
|
[#2503]: https://github.com/actix/actix-web/pull/2503
|
||||||
|
[#2510]: https://github.com/actix/actix-web/pull/2510
|
||||||
|
[#2546]: https://github.com/actix/actix-web/pull/2546
|
||||||
|
[#2553]: https://github.com/actix/actix-web/pull/2553
|
||||||
|
[#2555]: https://github.com/actix/actix-web/pull/2555
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>3.0.0 Pre-Releases</summary>
|
||||||
|
|
||||||
## 3.0.0-beta.21 - 2022-02-16
|
## 3.0.0-beta.21 - 2022-02-16
|
||||||
- No significant changes since `3.0.0-beta.20`.
|
- No significant changes since `3.0.0-beta.20`.
|
||||||
|
|
||||||
|
@ -170,6 +267,7 @@
|
||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## 2.0.3 - 2020-11-29
|
## 2.0.3 - 2020-11-29
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
name = "awc"
|
name = "awc"
|
||||||
version = "3.0.0-beta.21"
|
version = "3.0.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"fakeshadow <24548779@qq.com>",
|
"fakeshadow <24548779@qq.com>",
|
||||||
]
|
]
|
||||||
description = "Async HTTP and WebSocket client library built on the Actix ecosystem"
|
description = "Async HTTP and WebSocket client library"
|
||||||
keywords = ["actix", "http", "framework", "async", "web"]
|
keywords = ["actix", "http", "framework", "async", "web"]
|
||||||
categories = [
|
categories = [
|
||||||
"network-programming",
|
"network-programming",
|
||||||
|
@ -59,11 +59,11 @@ dangerous-h2c = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-service = "2.0.0"
|
actix-service = "2"
|
||||||
actix-http = { version = "3.0.0", features = ["http2", "ws"] }
|
actix-http = { version = "3", features = ["http2", "ws"] }
|
||||||
actix-rt = { version = "2.1", default-features = false }
|
actix-rt = { version = "2.1", default-features = false }
|
||||||
actix-tls = { version = "3", features = ["connect", "uri"] }
|
actix-tls = { version = "3", features = ["connect", "uri"] }
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3"
|
||||||
|
|
||||||
ahash = "0.7"
|
ahash = "0.7"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
|
@ -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", features = ["openssl"] }
|
actix-http = { version = "3", features = ["openssl"] }
|
||||||
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
|
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
|
||||||
actix-server = "2"
|
actix-server = "2"
|
||||||
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
|
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
|
||||||
actix-tls = { version = "3", features = ["openssl", "rustls"] }
|
actix-tls = { version = "3", features = ["openssl", "rustls"] }
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4.0.0", features = ["openssl"] }
|
actix-web = { version = "4", features = ["openssl"] }
|
||||||
|
|
||||||
brotli = "3.3.3"
|
brotli = "3.3.3"
|
||||||
const-str = "0.3"
|
const-str = "0.3"
|
||||||
|
@ -110,7 +110,7 @@ static_assertions = "1.1"
|
||||||
rcgen = "0.8"
|
rcgen = "0.8"
|
||||||
rustls-pemfile = "0.2"
|
rustls-pemfile = "0.2"
|
||||||
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
|
||||||
zstd = "0.10"
|
zstd = "0.11"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "client"
|
name = "client"
|
||||||
|
|
|
@ -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.21)
|
[](https://docs.rs/awc/3.0.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/awc/3.0.0-beta.21)
|
[](https://deps.rs/crate/awc/3.0.0)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
|
@ -246,7 +246,12 @@ where
|
||||||
///
|
///
|
||||||
/// The default limit size is 100.
|
/// The default limit size is 100.
|
||||||
pub fn limit(mut self, limit: usize) -> Self {
|
pub fn limit(mut self, limit: usize) -> Self {
|
||||||
|
if limit == 0 {
|
||||||
|
self.config.limit = u32::MAX as usize;
|
||||||
|
} else {
|
||||||
self.config.limit = limit;
|
self.config.limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,12 +83,12 @@ where
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
framed.send((head, body.size()).into()).await?;
|
|
||||||
|
|
||||||
let mut pin_framed = Pin::new(&mut framed);
|
let mut pin_framed = Pin::new(&mut framed);
|
||||||
|
|
||||||
// special handle for EXPECT request.
|
// special handle for EXPECT request.
|
||||||
let (do_send, mut res_head) = if is_expect {
|
let (do_send, mut res_head) = if is_expect {
|
||||||
|
pin_framed.send((head, body.size()).into()).await?;
|
||||||
|
|
||||||
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
|
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
|
||||||
.await
|
.await
|
||||||
.ok_or(ConnectError::Disconnected)??;
|
.ok_or(ConnectError::Disconnected)??;
|
||||||
|
@ -97,13 +97,17 @@ where
|
||||||
// and current head would be used as final response head.
|
// and current head would be used as final response head.
|
||||||
(head.status == StatusCode::CONTINUE, Some(head))
|
(head.status == StatusCode::CONTINUE, Some(head))
|
||||||
} else {
|
} else {
|
||||||
|
pin_framed.feed((head, body.size()).into()).await?;
|
||||||
|
|
||||||
(true, None)
|
(true, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
if do_send {
|
if do_send {
|
||||||
// send request body
|
// send request body
|
||||||
match body.size() {
|
match body.size() {
|
||||||
BodySize::None | BodySize::Sized(0) => {}
|
BodySize::None | BodySize::Sized(0) => {
|
||||||
|
poll_fn(|cx| pin_framed.as_mut().flush(cx)).await?;
|
||||||
|
}
|
||||||
_ => send_body(body, pin_framed.as_mut()).await?,
|
_ => send_body(body, pin_framed.as_mut()).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -30,17 +30,35 @@ pub type BoxConnectorService = Rc<
|
||||||
|
|
||||||
pub type BoxedSocket = Box<dyn ConnectionIo>;
|
pub type BoxedSocket = Box<dyn ConnectionIo>;
|
||||||
|
|
||||||
|
/// Combined HTTP and WebSocket request type received by connection service.
|
||||||
pub enum ConnectRequest {
|
pub enum ConnectRequest {
|
||||||
|
/// Standard HTTP request.
|
||||||
|
///
|
||||||
|
/// Contains the request head, body type, and optional pre-resolved socket address.
|
||||||
Client(RequestHeadType, AnyBody, Option<net::SocketAddr>),
|
Client(RequestHeadType, AnyBody, Option<net::SocketAddr>),
|
||||||
|
|
||||||
|
/// Tunnel used by WebSocket connection requests.
|
||||||
|
///
|
||||||
|
/// Contains the request head and optional pre-resolved socket address.
|
||||||
Tunnel(RequestHead, Option<net::SocketAddr>),
|
Tunnel(RequestHead, Option<net::SocketAddr>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Combined HTTP response & WebSocket tunnel type returned from connection service.
|
||||||
pub enum ConnectResponse {
|
pub enum ConnectResponse {
|
||||||
|
/// Standard HTTP response.
|
||||||
Client(ClientResponse),
|
Client(ClientResponse),
|
||||||
|
|
||||||
|
/// Tunnel used for WebSocket communication.
|
||||||
|
///
|
||||||
|
/// Contains response head and framed HTTP/1.1 codec.
|
||||||
Tunnel(ResponseHead, Framed<BoxedSocket, ClientCodec>),
|
Tunnel(ResponseHead, Framed<BoxedSocket, ClientCodec>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectResponse {
|
impl ConnectResponse {
|
||||||
|
/// Unwraps type into HTTP response.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if enum variant is not `Client`.
|
||||||
pub fn into_client_response(self) -> ClientResponse {
|
pub fn into_client_response(self) -> ClientResponse {
|
||||||
match self {
|
match self {
|
||||||
ConnectResponse::Client(res) => res,
|
ConnectResponse::Client(res) => res,
|
||||||
|
@ -50,6 +68,10 @@ impl ConnectResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unwraps type into WebSocket tunnel response.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if enum variant is not `Tunnel`.
|
||||||
pub fn into_tunnel_response(self) -> (ResponseHead, Framed<BoxedSocket, ClientCodec>) {
|
pub fn into_tunnel_response(self) -> (ResponseHead, Framed<BoxedSocket, ClientCodec>) {
|
||||||
match self {
|
match self {
|
||||||
ConnectResponse::Tunnel(head, framed) => (head, framed),
|
ConnectResponse::Tunnel(head, framed) => (head, framed),
|
||||||
|
@ -136,30 +158,37 @@ where
|
||||||
ConnectRequestProj::Connection { fut, req } => {
|
ConnectRequestProj::Connection { fut, req } => {
|
||||||
let connection = ready!(fut.poll(cx))?;
|
let connection = ready!(fut.poll(cx))?;
|
||||||
let req = req.take().unwrap();
|
let req = req.take().unwrap();
|
||||||
|
|
||||||
match req {
|
match req {
|
||||||
ConnectRequest::Client(head, body, ..) => {
|
ConnectRequest::Client(head, body, ..) => {
|
||||||
// send request
|
// send request
|
||||||
let fut = ConnectRequestFuture::Client {
|
let fut = ConnectRequestFuture::Client {
|
||||||
fut: connection.send_request(head, body),
|
fut: connection.send_request(head, body),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.set(fut);
|
self.set(fut);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectRequest::Tunnel(head, ..) => {
|
ConnectRequest::Tunnel(head, ..) => {
|
||||||
// send request
|
// send request
|
||||||
let fut = ConnectRequestFuture::Tunnel {
|
let fut = ConnectRequestFuture::Tunnel {
|
||||||
fut: connection.open_tunnel(RequestHeadType::from(head)),
|
fut: connection.open_tunnel(RequestHeadType::from(head)),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.set(fut);
|
self.set(fut);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.poll(cx)
|
self.poll(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectRequestProj::Client { fut } => {
|
ConnectRequestProj::Client { fut } => {
|
||||||
let (head, payload) = ready!(fut.as_mut().poll(cx))?;
|
let (head, payload) = ready!(fut.as_mut().poll(cx))?;
|
||||||
Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new(
|
Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new(
|
||||||
head, payload,
|
head, payload,
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectRequestProj::Tunnel { fut } => {
|
ConnectRequestProj::Tunnel { fut } => {
|
||||||
let (head, framed) = ready!(fut.as_mut().poll(cx))?;
|
let (head, framed) = ready!(fut.as_mut().poll(cx))?;
|
||||||
let framed = framed.into_map_io(|io| Box::new(io) as _);
|
let framed = framed.into_map_io(|io| Box::new(io) as _);
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
//! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem.
|
//! `awc` is an asynchronous HTTP and WebSocket client library.
|
||||||
//!
|
//!
|
||||||
//! # Making a GET request
|
//! # `GET` Requests
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! # #[actix_rt::main]
|
//! # #[actix_rt::main]
|
||||||
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
||||||
|
//! // create client
|
||||||
//! let mut client = awc::Client::default();
|
//! let mut client = awc::Client::default();
|
||||||
//! let response = client.get("http://www.rust-lang.org") // <- Create request builder
|
|
||||||
//! .insert_header(("User-Agent", "Actix-web"))
|
|
||||||
//! .send() // <- Send http request
|
|
||||||
//! .await?;
|
|
||||||
//!
|
//!
|
||||||
//! println!("Response: {:?}", response);
|
//! // construct request
|
||||||
|
//! let req = client.get("http://www.rust-lang.org")
|
||||||
|
//! .insert_header(("User-Agent", "awc/3.0"));
|
||||||
|
//!
|
||||||
|
//! // send request and await response
|
||||||
|
//! let res = req.send().await?;
|
||||||
|
//! println!("Response: {:?}", res);
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Making POST requests
|
//! # `POST` Requests
|
||||||
//! ## Raw body contents
|
//! ## Raw Body
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! # #[actix_rt::main]
|
//! # #[actix_rt::main]
|
||||||
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
||||||
|
@ -28,20 +31,6 @@
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Forms
|
|
||||||
//! ```no_run
|
|
||||||
//! # #[actix_rt::main]
|
|
||||||
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
|
||||||
//! let params = [("foo", "bar"), ("baz", "quux")];
|
|
||||||
//!
|
|
||||||
//! let mut client = awc::Client::default();
|
|
||||||
//! let response = client.post("http://httpbin.org/post")
|
|
||||||
//! .send_form(¶ms)
|
|
||||||
//! .await?;
|
|
||||||
//! # Ok(())
|
|
||||||
//! # }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ## JSON
|
//! ## JSON
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! # #[actix_rt::main]
|
//! # #[actix_rt::main]
|
||||||
|
@ -59,6 +48,20 @@
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
//! ## URL Encoded Form
|
||||||
|
//! ```no_run
|
||||||
|
//! # #[actix_rt::main]
|
||||||
|
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
||||||
|
//! let params = [("foo", "bar"), ("baz", "quux")];
|
||||||
|
//!
|
||||||
|
//! let mut client = awc::Client::default();
|
||||||
|
//! let response = client.post("http://httpbin.org/post")
|
||||||
|
//! .send_form(¶ms)
|
||||||
|
//! .await?;
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
//! # Response Compression
|
//! # Response Compression
|
||||||
//! All [official][iana-encodings] and common content encoding codecs are supported, optionally.
|
//! All [official][iana-encodings] and common content encoding codecs are supported, optionally.
|
||||||
//!
|
//!
|
||||||
|
@ -76,11 +79,12 @@
|
||||||
//!
|
//!
|
||||||
//! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
|
//! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
|
||||||
//!
|
//!
|
||||||
//! # WebSocket support
|
//! # WebSockets
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! # #[actix_rt::main]
|
//! # #[actix_rt::main]
|
||||||
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
//! use futures_util::{sink::SinkExt, stream::StreamExt};
|
//! use futures_util::{sink::SinkExt as _, stream::StreamExt as _};
|
||||||
|
//!
|
||||||
//! let (_resp, mut connection) = awc::Client::new()
|
//! let (_resp, mut connection) = awc::Client::new()
|
||||||
//! .ws("ws://echo.websocket.org")
|
//! .ws("ws://echo.websocket.org")
|
||||||
//! .connect()
|
//! .connect()
|
||||||
|
@ -89,8 +93,9 @@
|
||||||
//! connection
|
//! connection
|
||||||
//! .send(awc::ws::Message::Text("Echo".into()))
|
//! .send(awc::ws::Message::Text("Echo".into()))
|
||||||
//! .await?;
|
//! .await?;
|
||||||
|
//!
|
||||||
//! let response = connection.next().await.unwrap()?;
|
//! let response = connection.next().await.unwrap()?;
|
||||||
//! # assert_eq!(response, awc::ws::Frame::Text("Echo".as_bytes().into()));
|
//! assert_eq!(response, awc::ws::Frame::Text("Echo".into()));
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
|
@ -161,7 +161,8 @@ where
|
||||||
| StatusCode::SEE_OTHER
|
| StatusCode::SEE_OTHER
|
||||||
| StatusCode::TEMPORARY_REDIRECT
|
| StatusCode::TEMPORARY_REDIRECT
|
||||||
| StatusCode::PERMANENT_REDIRECT
|
| StatusCode::PERMANENT_REDIRECT
|
||||||
if *max_redirect_times > 0 =>
|
if *max_redirect_times > 0
|
||||||
|
&& res.headers().contains_key(header::LOCATION) =>
|
||||||
{
|
{
|
||||||
let reuse_body = res.head().status == StatusCode::TEMPORARY_REDIRECT
|
let reuse_body = res.head().status == StatusCode::TEMPORARY_REDIRECT
|
||||||
|| res.head().status == StatusCode::PERMANENT_REDIRECT;
|
|| res.head().status == StatusCode::PERMANENT_REDIRECT;
|
||||||
|
@ -245,26 +246,32 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result<Uri, SendRequestError> {
|
fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result<Uri, SendRequestError> {
|
||||||
let uri = res
|
// responses without this header are not processed
|
||||||
.headers()
|
let location = res.headers().get(header::LOCATION).unwrap();
|
||||||
.get(header::LOCATION)
|
|
||||||
.map(|value| {
|
// try to parse the location and resolve to a full URI but fall back to default if it fails
|
||||||
// try to parse the location to a full uri
|
let uri = Uri::try_from(location.as_bytes()).unwrap_or_else(|_| Uri::default());
|
||||||
let uri = Uri::try_from(value.as_bytes())
|
|
||||||
.map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?;
|
let uri = if uri.scheme().is_none() || uri.authority().is_none() {
|
||||||
if uri.scheme().is_none() || uri.authority().is_none() {
|
let builder = Uri::builder()
|
||||||
let uri = Uri::builder()
|
|
||||||
.scheme(prev_uri.scheme().cloned().unwrap())
|
.scheme(prev_uri.scheme().cloned().unwrap())
|
||||||
.authority(prev_uri.authority().cloned().unwrap())
|
.authority(prev_uri.authority().cloned().unwrap());
|
||||||
.path_and_query(value.as_bytes())
|
|
||||||
.build()?;
|
// when scheme or authority is missing treat the location value as path and query
|
||||||
Ok::<_, SendRequestError>(uri)
|
// recover error where location does not have leading slash
|
||||||
|
let path = if location.as_bytes().starts_with(b"/") {
|
||||||
|
location.as_bytes().to_owned()
|
||||||
} else {
|
} else {
|
||||||
Ok(uri)
|
[b"/", location.as_bytes()].concat()
|
||||||
}
|
};
|
||||||
})
|
|
||||||
// TODO: this error type is wrong.
|
builder
|
||||||
.ok_or(SendRequestError::Url(InvalidUrl::MissingScheme))??;
|
.path_and_query(path)
|
||||||
|
.build()
|
||||||
|
.map_err(|err| SendRequestError::Url(InvalidUrl::HttpError(err)))?
|
||||||
|
} else {
|
||||||
|
uri
|
||||||
|
};
|
||||||
|
|
||||||
Ok(uri)
|
Ok(uri)
|
||||||
}
|
}
|
||||||
|
@ -287,10 +294,13 @@ mod tests {
|
||||||
use actix_web::{web, App, Error, HttpRequest, HttpResponse};
|
use actix_web::{web, App, Error, HttpRequest, HttpResponse};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{http::header::HeaderValue, ClientBuilder};
|
use crate::{
|
||||||
|
http::{header::HeaderValue, StatusCode},
|
||||||
|
ClientBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_basic_redirect() {
|
async fn basic_redirect() {
|
||||||
let client = ClientBuilder::new()
|
let client = ClientBuilder::new()
|
||||||
.disable_redirects()
|
.disable_redirects()
|
||||||
.wrap(Redirect::new().max_redirect_times(10))
|
.wrap(Redirect::new().max_redirect_times(10))
|
||||||
|
@ -315,6 +325,44 @@ mod tests {
|
||||||
assert_eq!(res.status().as_u16(), 400);
|
assert_eq!(res.status().as_u16(), 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn redirect_relative_without_leading_slash() {
|
||||||
|
let client = ClientBuilder::new().finish();
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.service(web::resource("/").route(web::to(|| async {
|
||||||
|
HttpResponse::Found()
|
||||||
|
.insert_header(("location", "abc/"))
|
||||||
|
.finish()
|
||||||
|
})))
|
||||||
|
.service(
|
||||||
|
web::resource("/abc/")
|
||||||
|
.route(web::to(|| async { HttpResponse::Accepted().finish() })),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let res = client.get(srv.url("/")).send().await.unwrap();
|
||||||
|
assert_eq!(res.status(), StatusCode::ACCEPTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn redirect_without_location() {
|
||||||
|
let client = ClientBuilder::new()
|
||||||
|
.disable_redirects()
|
||||||
|
.wrap(Redirect::new().max_redirect_times(10))
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new().service(web::resource("/").route(web::to(|| async {
|
||||||
|
Ok::<_, Error>(HttpResponse::Found().finish())
|
||||||
|
})))
|
||||||
|
});
|
||||||
|
|
||||||
|
let res = client.get(srv.url("/")).send().await.unwrap();
|
||||||
|
assert_eq!(res.status(), StatusCode::FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_redirect_limit() {
|
async fn test_redirect_limit() {
|
||||||
let client = ClientBuilder::new()
|
let client = ClientBuilder::new()
|
||||||
|
@ -328,14 +376,14 @@ mod tests {
|
||||||
.service(web::resource("/").route(web::to(|| async {
|
.service(web::resource("/").route(web::to(|| async {
|
||||||
Ok::<_, Error>(
|
Ok::<_, Error>(
|
||||||
HttpResponse::Found()
|
HttpResponse::Found()
|
||||||
.append_header(("location", "/test"))
|
.insert_header(("location", "/test"))
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
})))
|
})))
|
||||||
.service(web::resource("/test").route(web::to(|| async {
|
.service(web::resource("/test").route(web::to(|| async {
|
||||||
Ok::<_, Error>(
|
Ok::<_, Error>(
|
||||||
HttpResponse::Found()
|
HttpResponse::Found()
|
||||||
.append_header(("location", "/test2"))
|
.insert_header(("location", "/test2"))
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
})))
|
})))
|
||||||
|
@ -345,8 +393,15 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let res = client.get(srv.url("/")).send().await.unwrap();
|
let res = client.get(srv.url("/")).send().await.unwrap();
|
||||||
|
assert_eq!(res.status(), StatusCode::FOUND);
|
||||||
assert_eq!(res.status().as_u16(), 302);
|
assert_eq!(
|
||||||
|
res.headers()
|
||||||
|
.get(header::LOCATION)
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"/test2"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
|
|
@ -505,7 +505,7 @@ impl fmt::Debug for ClientRequest {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
"\nClientRequest {:?} {}:{}",
|
"\nClientRequest {:?} {} {}",
|
||||||
self.head.version, self.head.method, self.head.uri
|
self.head.version, self.head.method, self.head.uri
|
||||||
)?;
|
)?;
|
||||||
writeln!(f, " headers:")?;
|
writeln!(f, " headers:")?;
|
||||||
|
|
|
@ -160,7 +160,7 @@ where
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// `Future` implementation returns error if:
|
/// `Future` implementation returns error if:
|
||||||
/// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB)
|
/// - content length is greater than [limit](ResponseBody::limit) (default: 2 MiB)
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
|
|
Loading…
Reference in New Issue