mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-30 12:06:41 +02:00
Compare commits
42 Commits
codegen-v4
...
pr/cptrodo
Author | SHA1 | Date | |
---|---|---|---|
|
0216cb186e | ||
|
2b543437c3 | ||
|
97399e8c8c | ||
|
8dee8a1426 | ||
|
e68f87f84f | ||
|
0f3068f488 | ||
|
a3ec0ccf99 | ||
|
e9a1aeebce | ||
|
14b09e35d1 | ||
|
2bfc170fb0 | ||
|
42fd6290d6 | ||
|
5e29726c4f | ||
|
442fa279da | ||
|
bfdc29ebb8 | ||
|
0e7380659f | ||
|
44c5cdaa10 | ||
|
9e7a6fe57b | ||
|
dfaca18584 | ||
|
19c9d858f2 | ||
|
4131786127 | ||
|
0ba147ef71 | ||
|
3fc01c4887 | ||
|
4c4024c949 | ||
|
c96844ab14 | ||
|
68adcf657a | ||
|
99bccb74ac | ||
|
e0939a01fc | ||
|
20c7c07dc0 | ||
|
d7c6774ad5 | ||
|
67efa4a4db | ||
|
d77bcb0b7c | ||
|
c4db9a1ae2 | ||
|
740d0c0c9d | ||
|
f27584046c | ||
|
129b78f9c7 | ||
|
ad27150c5f | ||
|
8d5d6a2598 | ||
|
e97329eb2a | ||
|
fbfff3e751 | ||
|
fdfb3d45db | ||
|
4e05629368 | ||
|
e35ec28cd2 |
8
.github/workflows/bench.yml
vendored
8
.github/workflows/bench.yml
vendored
@@ -6,14 +6,18 @@ on:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check_benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
16
.github/workflows/ci-post-merge.yml
vendored
16
.github/workflows/ci-post-merge.yml
vendored
@@ -5,7 +5,11 @@ on:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build_and_test_nightly:
|
||||
@@ -29,7 +33,7 @@ jobs:
|
||||
CARGO_UNSTABLE_SPARSE_REGISTRY: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# install OpenSSL on Windows
|
||||
# TODO: GitHub actions docs state that OpenSSL is
|
||||
@@ -44,7 +48,7 @@ jobs:
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||
toolchain: ${{ matrix.version }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
@@ -81,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
|
||||
cargo install cargo-cache --version 0.8.3 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
ci_feature_powerset_check:
|
||||
@@ -93,7 +97,7 @@ jobs:
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -120,7 +124,7 @@ jobs:
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
|
72
.github/workflows/ci.yml
vendored
72
.github/workflows/ci.yml
vendored
@@ -7,7 +7,11 @@ on:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
@@ -17,7 +21,7 @@ jobs:
|
||||
target:
|
||||
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
|
||||
version:
|
||||
- 1.59.0 # MSRV
|
||||
- stable
|
||||
@@ -25,30 +29,22 @@ jobs:
|
||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||
runs-on: ${{ matrix.target.os }}
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
CARGO_INCREMENTAL: 0
|
||||
VCPKGRS_DYNAMIC: 1
|
||||
env: {}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# install OpenSSL on Windows
|
||||
# TODO: GitHub actions docs state that OpenSSL is
|
||||
# already installed on these Windows machines somewhere
|
||||
- name: Set vcpkg root
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: vcpkg install openssl:x64-windows
|
||||
if: matrix.target.os == 'windows-latest'
|
||||
run: choco install openssl
|
||||
- name: Set OpenSSL dir in env
|
||||
if: matrix.target.os == 'windows-latest'
|
||||
run: echo 'OPENSSL_DIR=C:\Program Files\OpenSSL-Win64' | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
- name: Install Rust (${{ matrix.version }})
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||
profile: minimal
|
||||
override: true
|
||||
toolchain: ${{ matrix.version }}
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
@@ -60,12 +56,6 @@ jobs:
|
||||
cargo add const-str@0.3 --dev -p=actix-web
|
||||
cargo add const-str@0.3 --dev -p=awc
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: workaround MSRV issues
|
||||
if: matrix.version != 'stable'
|
||||
run: |
|
||||
@@ -95,42 +85,34 @@ jobs:
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
|
||||
cargo install cargo-cache --version 0.8.3 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
io-uring:
|
||||
name: io-uring tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
run: cargo generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: { toolchain: nightly }
|
||||
|
||||
- name: tests (io-uring)
|
||||
timeout-minutes: 60
|
||||
run: >
|
||||
sudo bash -c "ulimit -Sl 512
|
||||
&& ulimit -Hl 512
|
||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||
&& RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
|
||||
sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && PATH=$PATH:/usr/share/rust/.cargo/bin && RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
|
||||
|
||||
|
||||
rustdoc:
|
||||
name: doc tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
run: cargo generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: { toolchain: nightly }
|
||||
|
||||
- name: doc tests
|
||||
run: cargo ci-doctest
|
||||
|
68
.github/workflows/clippy-fmt.yml
vendored
68
.github/workflows/clippy-fmt.yml
vendored
@@ -4,46 +4,72 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with: { components: rustfmt }
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
permissions:
|
||||
checks: write # to add clippy checks to PR diffs
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: { components: clippy }
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
run: cargo generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Check with Clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
- uses: giraffate/clippy-action@v1
|
||||
with:
|
||||
args: --workspace --tests --examples --all-features
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: 'github-pr-check'
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
clippy_flags: --workspace --all-features --tests --examples --bins -- -Dclippy::todo
|
||||
|
||||
lint-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: { components: rust-docs }
|
||||
|
||||
- name: Check for broken intra-doc links
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
env: { RUSTDOCFLAGS: "-D warnings" }
|
||||
run: cargo doc --no-deps --all-features --workspace
|
||||
|
||||
public-api-diff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
command: doc
|
||||
args: --no-deps --all-features --workspace
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: { toolchain: nightly }
|
||||
|
||||
- uses: taiki-e/cache-cargo-install-action@v1
|
||||
with: { tool: cargo-public-api }
|
||||
|
||||
- name: generate API diff
|
||||
run: |
|
||||
for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do
|
||||
cargo public-api --manifest-path "$f" diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }}
|
||||
done
|
||||
|
23
.github/workflows/coverage.yml
vendored
23
.github/workflows/coverage.yml
vendored
@@ -6,26 +6,23 @@ on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# job currently (1st Feb 2022) segfaults
|
||||
coverage:
|
||||
name: coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- 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
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: { toolchain: nightly }
|
||||
|
||||
- name: Generate coverage file
|
||||
run: |
|
||||
|
12
.github/workflows/upload-doc.yml
vendored
12
.github/workflows/upload-doc.yml
vendored
@@ -4,16 +4,22 @@ on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write # to push changes in repo (jamesives/github-pages-deploy-action)
|
||||
contents: write # to push changes in repo (jamesives/github-pages-deploy-action)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
## Unreleased - 2023-xx-xx
|
||||
|
||||
## 0.6.3 - 2023-01-21
|
||||
|
||||
|
@@ -32,11 +32,11 @@ derive_more = "0.99.5"
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
http-range = "0.1.4"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
mime = "0.3.9"
|
||||
mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2.7"
|
||||
v_htmlescape= "0.15"
|
||||
v_htmlescape = "0.15.5"
|
||||
|
||||
# experimental-io-uring
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
|
@@ -4,46 +4,45 @@ use derive_more::Display;
|
||||
/// Errors which can occur when serving static files.
|
||||
#[derive(Debug, PartialEq, Eq, Display)]
|
||||
pub enum FilesError {
|
||||
/// Path is not a directory
|
||||
/// Path is not a directory.
|
||||
#[allow(dead_code)]
|
||||
#[display(fmt = "Path is not a directory. Unable to serve static files")]
|
||||
#[display(fmt = "path is not a directory. Unable to serve static files")]
|
||||
IsNotDirectory,
|
||||
|
||||
/// Cannot render directory
|
||||
#[display(fmt = "Unable to render directory without index file")]
|
||||
/// Cannot render directory.
|
||||
#[display(fmt = "unable to render directory without index file")]
|
||||
IsDirectory,
|
||||
}
|
||||
|
||||
/// Return `NotFound` for `FilesError`
|
||||
impl ResponseError for FilesError {
|
||||
/// Returns `404 Not Found`.
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::NOT_FOUND
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug, PartialEq, Eq, Display)]
|
||||
#[non_exhaustive]
|
||||
pub enum UriSegmentError {
|
||||
/// The segment started with the wrapped invalid character.
|
||||
#[display(fmt = "The segment started with the wrapped invalid character")]
|
||||
/// Segment started with the wrapped invalid character.
|
||||
#[display(fmt = "segment started with invalid character: ('{_0}')")]
|
||||
BadStart(char),
|
||||
|
||||
/// The segment contained the wrapped invalid character.
|
||||
#[display(fmt = "The segment contained the wrapped invalid character")]
|
||||
/// Segment contained the wrapped invalid character.
|
||||
#[display(fmt = "segment contained invalid character ('{_0}')")]
|
||||
BadChar(char),
|
||||
|
||||
/// The segment ended with the wrapped invalid character.
|
||||
#[display(fmt = "The segment ended with the wrapped invalid character")]
|
||||
/// Segment ended with the wrapped invalid character.
|
||||
#[display(fmt = "segment ended with invalid character: ('{_0}')")]
|
||||
BadEnd(char),
|
||||
|
||||
/// The path is not a valid UTF-8 string after doing percent decoding.
|
||||
#[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")]
|
||||
/// Path is not a valid UTF-8 string after percent-decoding.
|
||||
#[display(fmt = "path is not a valid UTF-8 string after percent-decoding")]
|
||||
NotValidUtf8,
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UriSegmentError`
|
||||
impl ResponseError for UriSegmentError {
|
||||
/// Returns `400 Bad Request`.
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
|
@@ -14,6 +14,9 @@
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||
use actix_web::{
|
||||
|
@@ -1,4 +1,36 @@
|
||||
use derive_more::{Display, Error};
|
||||
use std::fmt;
|
||||
|
||||
use derive_more::Error;
|
||||
|
||||
/// Copy of `http_range::HttpRangeParseError`.
|
||||
#[derive(Debug, Clone)]
|
||||
enum HttpRangeParseError {
|
||||
InvalidRange,
|
||||
NoOverlap,
|
||||
}
|
||||
|
||||
impl From<http_range::HttpRangeParseError> for HttpRangeParseError {
|
||||
fn from(err: http_range::HttpRangeParseError) -> Self {
|
||||
match err {
|
||||
http_range::HttpRangeParseError::InvalidRange => Self::InvalidRange,
|
||||
http_range::HttpRangeParseError::NoOverlap => Self::NoOverlap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
#[non_exhaustive]
|
||||
pub struct ParseRangeErr(#[error(not(source))] HttpRangeParseError);
|
||||
|
||||
impl fmt::Display for ParseRangeErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("invalid Range header: ")?;
|
||||
f.write_str(match self.0 {
|
||||
HttpRangeParseError::InvalidRange => "invalid syntax",
|
||||
HttpRangeParseError::NoOverlap => "range starts after end of content",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP Range header representation.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -10,26 +42,22 @@ pub struct HttpRange {
|
||||
pub length: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
#[display(fmt = "Parse HTTP Range failed")]
|
||||
pub struct ParseRangeErr(#[error(not(source))] ());
|
||||
|
||||
impl HttpRange {
|
||||
/// Parses Range HTTP header string as per RFC 2616.
|
||||
///
|
||||
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
||||
/// `size` is full size of response (file).
|
||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
|
||||
match http_range::HttpRange::parse(header, size) {
|
||||
Ok(ranges) => Ok(ranges
|
||||
.iter()
|
||||
.map(|range| HttpRange {
|
||||
start: range.start,
|
||||
length: range.length,
|
||||
})
|
||||
.collect()),
|
||||
Err(_) => Err(ParseRangeErr(())),
|
||||
}
|
||||
let ranges = http_range::HttpRange::parse(header, size)
|
||||
.map_err(|err| ParseRangeErr(err.into()))?;
|
||||
|
||||
Ok(ranges
|
||||
.iter()
|
||||
.map(|range| HttpRange {
|
||||
start: range.start,
|
||||
length: range.length,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
## Unreleased - 2023-xx-xx
|
||||
|
||||
## 3.1.0 - 2023-01-21
|
||||
|
||||
|
@@ -39,15 +39,15 @@ awc = { version = "3", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.17", default-features = false }
|
||||
http = "0.2.5"
|
||||
http = "0.2.7"
|
||||
log = "0.4"
|
||||
socket2 = "0.4"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
tokio = { version = "1.18.5", features = ["sync"] }
|
||||
tokio = { version = "1.24.2", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||
|
@@ -5,6 +5,7 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
extern crate tls_openssl as openssl;
|
||||
@@ -33,7 +34,9 @@ use tokio::sync::mpsc;
|
||||
/// ```no_run
|
||||
/// use actix_http::HttpService;
|
||||
/// use actix_http_test::test_server;
|
||||
/// use actix_web::{web, App, HttpResponse, Error};
|
||||
/// use actix_service::map_config;
|
||||
/// use actix_service::ServiceFactoryExt;
|
||||
/// use actix_web::{dev::AppConfig, web, App, Error, HttpResponse};
|
||||
///
|
||||
/// async fn my_handler() -> Result<HttpResponse, Error> {
|
||||
/// Ok(HttpResponse::Ok().into())
|
||||
@@ -41,14 +44,19 @@ use tokio::sync::mpsc;
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_example() {
|
||||
/// let mut srv = TestServer::start(||
|
||||
/// HttpService::new(
|
||||
/// App::new().service(web::resource("/").to(my_handler))
|
||||
/// )
|
||||
/// );
|
||||
/// let srv = test_server(|| {
|
||||
/// let app = App::new().service(web::resource("/").to(my_handler));
|
||||
///
|
||||
/// HttpService::build()
|
||||
/// .h1(map_config(app, |_| AppConfig::default()))
|
||||
/// .tcp()
|
||||
/// .map_err(|_| ())
|
||||
/// })
|
||||
/// .await;
|
||||
///
|
||||
/// let req = srv.get("/");
|
||||
/// let response = req.send().await.unwrap();
|
||||
///
|
||||
/// assert!(response.status().is_success());
|
||||
/// }
|
||||
/// ```
|
||||
|
@@ -1,6 +1,17 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
## Unreleased - 2023-xx-xx
|
||||
|
||||
### Added
|
||||
|
||||
- Add `body::to_body_limit()` function.
|
||||
- Add `body::BodyLimitExceeded` error type.
|
||||
|
||||
## 3.3.1 - 2023-03-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use correct `http` version requirement to ensure support for const `HeaderName` definitions.
|
||||
|
||||
## 3.3.0 - 2023-01-21
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.3.0"
|
||||
version = "3.3.1"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@@ -61,23 +61,23 @@ actix-codec = "0.5"
|
||||
actix-utils = "3"
|
||||
actix-rt = { version = "2.2", default-features = false }
|
||||
|
||||
ahash = "0.7"
|
||||
ahash = "0.8"
|
||||
bitflags = "1.2"
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.5"
|
||||
http = "0.2.7"
|
||||
httparse = "1.5.1"
|
||||
httpdate = "1.0.1"
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
mime = "0.3"
|
||||
mime = "0.3.4"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2"
|
||||
smallvec = "1.6.1"
|
||||
tokio = { version = "1.18.5", features = [] }
|
||||
tokio = { version = "1.24.2", features = [] }
|
||||
tokio-util = { version = "0.7", features = ["io", "codec"] }
|
||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
@@ -119,24 +119,13 @@ serde_json = "1.0"
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
tokio = { version = "1.18.5", features = ["net", "rt", "macros"] }
|
||||
tokio = { version = "1.24.2", features = ["net", "rt", "macros"] }
|
||||
|
||||
[[example]]
|
||||
name = "ws"
|
||||
required-features = ["ws", "rustls"]
|
||||
|
||||
[[bench]]
|
||||
name = "write-camel-case"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "status-line"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "uninit-headers"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "quality-value"
|
||||
name = "response-body-compression"
|
||||
harness = false
|
||||
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
|
||||
|
@@ -3,11 +3,11 @@
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.3.0)
|
||||
[](https://docs.rs/actix-http/3.3.1)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.3.0)
|
||||
[](https://deps.rs/crate/actix-http/3.3.1)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@@ -1,97 +0,0 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||
|
||||
fn bench_quality_display_impls(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("quality value display impls");
|
||||
|
||||
for i in CODES.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
|
||||
b.iter(|| _new::Quality(i).to_string())
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||
b.iter(|| _naive::Quality(i).to_string())
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_quality_display_impls);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _new {
|
||||
use std::fmt;
|
||||
|
||||
pub struct Quality(pub(crate) u16);
|
||||
|
||||
impl fmt::Display for Quality {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
0 => f.write_str("0"),
|
||||
1000 => f.write_str("1"),
|
||||
|
||||
// some number in the range 1–999
|
||||
x => {
|
||||
f.write_str("0.")?;
|
||||
|
||||
// this implementation avoids string allocation otherwise required
|
||||
// for `.trim_end_matches('0')`
|
||||
|
||||
if x < 10 {
|
||||
f.write_str("00")?;
|
||||
// 0 is handled so it's not possible to have a trailing 0, we can just return
|
||||
itoa_fmt(f, x)
|
||||
} else if x < 100 {
|
||||
f.write_str("0")?;
|
||||
if x % 10 == 0 {
|
||||
// trailing 0, divide by 10 and write
|
||||
itoa_fmt(f, x / 10)
|
||||
} else {
|
||||
itoa_fmt(f, x)
|
||||
}
|
||||
} else {
|
||||
// x is in range 101–999
|
||||
|
||||
if x % 100 == 0 {
|
||||
// two trailing 0s, divide by 100 and write
|
||||
itoa_fmt(f, x / 100)
|
||||
} else if x % 10 == 0 {
|
||||
// one trailing 0, divide by 10 and write
|
||||
itoa_fmt(f, x / 10)
|
||||
} else {
|
||||
itoa_fmt(f, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
|
||||
let mut buf = itoa::Buffer::new();
|
||||
wr.write_str(buf.format(value))
|
||||
}
|
||||
}
|
||||
|
||||
mod _naive {
|
||||
use std::fmt;
|
||||
|
||||
pub struct Quality(pub(crate) u16);
|
||||
|
||||
impl fmt::Display for Quality {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
0 => f.write_str("0"),
|
||||
1000 => f.write_str("1"),
|
||||
|
||||
x => {
|
||||
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
actix-http/benches/response-body-compression.rs
Normal file
90
actix-http/benches/response-body-compression.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use std::convert::Infallible;
|
||||
|
||||
use actix_http::{encoding::Encoder, ContentEncoding, Request, Response, StatusCode};
|
||||
use actix_service::{fn_service, Service as _};
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
static BODY: &[u8] = include_bytes!("../Cargo.toml");
|
||||
|
||||
fn compression_responses(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("compression responses");
|
||||
|
||||
group.bench_function("identity", |b| {
|
||||
let rt = actix_rt::Runtime::new().unwrap();
|
||||
|
||||
let identity_svc = fn_service(|_: Request| async move {
|
||||
let mut res = Response::with_body(StatusCode::OK, ());
|
||||
let body = black_box(Encoder::response(
|
||||
ContentEncoding::Identity,
|
||||
res.head_mut(),
|
||||
BODY,
|
||||
));
|
||||
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
rt.block_on(identity_svc.call(Request::new())).unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("gzip", |b| {
|
||||
let rt = actix_rt::Runtime::new().unwrap();
|
||||
|
||||
let identity_svc = fn_service(|_: Request| async move {
|
||||
let mut res = Response::with_body(StatusCode::OK, ());
|
||||
let body = black_box(Encoder::response(
|
||||
ContentEncoding::Gzip,
|
||||
res.head_mut(),
|
||||
BODY,
|
||||
));
|
||||
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
rt.block_on(identity_svc.call(Request::new())).unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("br", |b| {
|
||||
let rt = actix_rt::Runtime::new().unwrap();
|
||||
|
||||
let identity_svc = fn_service(|_: Request| async move {
|
||||
let mut res = Response::with_body(StatusCode::OK, ());
|
||||
let body = black_box(Encoder::response(
|
||||
ContentEncoding::Brotli,
|
||||
res.head_mut(),
|
||||
BODY,
|
||||
));
|
||||
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
rt.block_on(identity_svc.call(Request::new())).unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("zstd", |b| {
|
||||
let rt = actix_rt::Runtime::new().unwrap();
|
||||
|
||||
let identity_svc = fn_service(|_: Request| async move {
|
||||
let mut res = Response::with_body(StatusCode::OK, ());
|
||||
let body = black_box(Encoder::response(
|
||||
ContentEncoding::Zstd,
|
||||
res.head_mut(),
|
||||
BODY,
|
||||
));
|
||||
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
rt.block_on(identity_svc.call(Request::new())).unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, compression_responses);
|
||||
criterion_main!(benches);
|
@@ -1,214 +0,0 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use http::Version;
|
||||
|
||||
const CODES: &[u16] = &[201, 303, 404, 515];
|
||||
|
||||
fn bench_write_status_line_11(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_status_line v1.1");
|
||||
|
||||
let version = Version::HTTP_11;
|
||||
|
||||
for i in CODES.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_original::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_new::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_naive::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_write_status_line_10(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_status_line v1.0");
|
||||
|
||||
let version = Version::HTTP_10;
|
||||
|
||||
for i in CODES.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_original::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_new::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_naive::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_write_status_line_09(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_status_line v0.9");
|
||||
|
||||
let version = Version::HTTP_09;
|
||||
|
||||
for i in CODES.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_original::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_new::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_naive::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_write_status_line_11,
|
||||
bench_write_status_line_10,
|
||||
bench_write_status_line_09
|
||||
);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _naive {
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use http::Version;
|
||||
|
||||
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
|
||||
match version {
|
||||
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
|
||||
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
|
||||
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
|
||||
_ => {
|
||||
// other HTTP version handlers do not use this method
|
||||
}
|
||||
}
|
||||
|
||||
bytes.put_slice(n.to_string().as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
mod _new {
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use http::Version;
|
||||
|
||||
const DIGITS_START: u8 = b'0';
|
||||
|
||||
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
|
||||
match version {
|
||||
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
|
||||
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
|
||||
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
|
||||
_ => {
|
||||
// other HTTP version handlers do not use this method
|
||||
}
|
||||
}
|
||||
|
||||
let d100 = (n / 100) as u8;
|
||||
let d10 = ((n / 10) % 10) as u8;
|
||||
let d1 = (n % 10) as u8;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d100);
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
|
||||
bytes.put_u8(b' ');
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
use std::ptr;
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use http::Version;
|
||||
|
||||
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
|
||||
2021222324252627282930313233343536373839\
|
||||
4041424344454647484950515253545556575859\
|
||||
6061626364656667686970717273747576777879\
|
||||
8081828384858687888990919293949596979899";
|
||||
|
||||
pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13;
|
||||
|
||||
pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) {
|
||||
let mut buf: [u8; STATUS_LINE_BUF_SIZE] = *b"HTTP/1.1 ";
|
||||
|
||||
match version {
|
||||
Version::HTTP_2 => buf[5] = b'2',
|
||||
Version::HTTP_10 => buf[7] = b'0',
|
||||
Version::HTTP_09 => {
|
||||
buf[5] = b'0';
|
||||
buf[7] = b'9';
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut curr: isize = 12;
|
||||
let buf_ptr = buf.as_mut_ptr();
|
||||
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
|
||||
let four = n > 999;
|
||||
|
||||
// decode 2 more chars, if > 2 chars
|
||||
let d1 = (n % 100) << 1;
|
||||
n /= 100;
|
||||
curr -= 2;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
|
||||
// decode last 1 or 2 chars
|
||||
if n < 10 {
|
||||
curr -= 1;
|
||||
unsafe {
|
||||
*buf_ptr.offset(curr) = (n as u8) + b'0';
|
||||
}
|
||||
} else {
|
||||
let d1 = n << 1;
|
||||
curr -= 2;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
}
|
||||
|
||||
bytes.put_slice(&buf);
|
||||
if four {
|
||||
bytes.put_u8(b' ');
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,135 +0,0 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
use bytes::BytesMut;
|
||||
|
||||
// A Miri run detects UB, seen on this playground:
|
||||
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915
|
||||
|
||||
fn bench_header_parsing(c: &mut Criterion) {
|
||||
c.bench_function("Original (Unsound) [short]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ_SHORT);
|
||||
_original::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("New (safe) [short]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ_SHORT);
|
||||
_new::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("Original (Unsound) [realistic]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ);
|
||||
_original::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("New (safe) [realistic]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ);
|
||||
_new::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_header_parsing);
|
||||
criterion_main!(benches);
|
||||
|
||||
const MAX_HEADERS: usize = 96;
|
||||
|
||||
const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
||||
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct HeaderIndex {
|
||||
name: (usize, usize),
|
||||
value: (usize, usize),
|
||||
}
|
||||
|
||||
const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
||||
name: (0, 0),
|
||||
value: (0, 0),
|
||||
};
|
||||
|
||||
const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = [EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||
|
||||
impl HeaderIndex {
|
||||
fn record(bytes: &[u8], headers: &[httparse::Header<'_>], indices: &mut [HeaderIndex]) {
|
||||
let bytes_ptr = bytes.as_ptr() as usize;
|
||||
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
|
||||
let name_start = header.name.as_ptr() as usize - bytes_ptr;
|
||||
let name_end = name_start + header.name.len();
|
||||
indices.name = (name_start, name_end);
|
||||
let value_start = header.value.as_ptr() as usize - bytes_ptr;
|
||||
let value_end = value_start + header.value.len();
|
||||
indices.value = (value_start, value_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test cases taken from:
|
||||
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
|
||||
|
||||
const REQ_SHORT: &[u8] = b"\
|
||||
GET / HTTP/1.0\r\n\
|
||||
Host: example.com\r\n\
|
||||
Cookie: session=60; user_id=1\r\n\r\n";
|
||||
|
||||
const REQ: &[u8] = b"\
|
||||
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
|
||||
Host: www.kittyhell.com\r\n\
|
||||
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\
|
||||
Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\
|
||||
Accept-Encoding: gzip,deflate\r\n\
|
||||
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\
|
||||
Keep-Alive: 115\r\n\
|
||||
Connection: keep-alive\r\n\
|
||||
Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n";
|
||||
|
||||
mod _new {
|
||||
use super::*;
|
||||
|
||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||
|
||||
let mut req = httparse::Request::new(&mut parsed);
|
||||
match req.parse(src).unwrap() {
|
||||
httparse::Status::Complete(_len) => {
|
||||
HeaderIndex::record(src, req.headers, &mut headers);
|
||||
req.headers.len()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
use super::*;
|
||||
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||
#![allow(invalid_value, clippy::uninit_assumed_init)]
|
||||
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
#[allow(invalid_value)]
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
let mut req = httparse::Request::new(&mut parsed);
|
||||
match req.parse(src).unwrap() {
|
||||
httparse::Status::Complete(_len) => {
|
||||
HeaderIndex::record(src, req.headers, &mut headers);
|
||||
req.headers.len()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
fn bench_write_camel_case(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_camel_case");
|
||||
|
||||
let names = ["connection", "Transfer-Encoding", "transfer-encoding"];
|
||||
|
||||
for &i in &names {
|
||||
let bts = i.as_bytes();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Original", i), bts, |b, bts| {
|
||||
b.iter(|| {
|
||||
let mut buf = black_box([0; 24]);
|
||||
_original::write_camel_case(black_box(bts), &mut buf)
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
|
||||
b.iter(|| {
|
||||
let mut buf = black_box([0; 24]);
|
||||
let len = black_box(bts.len());
|
||||
_new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_write_camel_case);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _new {
|
||||
pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
|
||||
// first copy entire (potentially wrong) slice to output
|
||||
let buffer = unsafe {
|
||||
std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len);
|
||||
std::slice::from_raw_parts_mut(buf, len)
|
||||
};
|
||||
|
||||
let mut iter = value.iter();
|
||||
|
||||
// first character should be uppercase
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[0] = c & 0b1101_1111;
|
||||
}
|
||||
|
||||
// track 1 ahead of the current position since that's the location being assigned to
|
||||
let mut index = 2;
|
||||
|
||||
// remaining characters after hyphens should also be uppercase
|
||||
while let Some(&c) = iter.next() {
|
||||
if c == b'-' {
|
||||
// advance iter by one and uppercase if needed
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[index] = c & 0b1101_1111;
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
||||
let mut index = 0;
|
||||
let key = value;
|
||||
let mut key_iter = key.iter();
|
||||
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
while let Some(c) = key_iter.next() {
|
||||
buffer[index] = *c;
|
||||
index += 1;
|
||||
if *c == b'-' {
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,4 +22,4 @@ pub(crate) use self::message_body::MessageBodyMapErr;
|
||||
pub use self::none::None;
|
||||
pub use self::size::BodySize;
|
||||
pub use self::sized_stream::SizedStream;
|
||||
pub use self::utils::to_bytes;
|
||||
pub use self::utils::{to_bytes, to_bytes_limited, BodyLimitExceeded};
|
||||
|
@@ -3,75 +3,196 @@ use std::task::Poll;
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use derive_more::{Display, Error};
|
||||
use futures_core::ready;
|
||||
|
||||
use super::{BodySize, MessageBody};
|
||||
|
||||
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
|
||||
/// Collects all the bytes produced by `body`.
|
||||
///
|
||||
/// Any errors produced by the body stream are returned immediately.
|
||||
///
|
||||
/// Consider using [`to_bytes_limited`] instead to protect against memory exhaustion.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use actix_http::body::{self, to_bytes};
|
||||
/// use bytes::Bytes;
|
||||
///
|
||||
/// # async fn test_to_bytes() {
|
||||
/// # actix_rt::System::new().block_on(async {
|
||||
/// let body = body::None::new();
|
||||
/// let bytes = to_bytes(body).await.unwrap();
|
||||
/// assert!(bytes.is_empty());
|
||||
///
|
||||
/// let body = Bytes::from_static(b"123");
|
||||
/// let bytes = to_bytes(body).await.unwrap();
|
||||
/// assert_eq!(bytes, b"123"[..]);
|
||||
/// # }
|
||||
/// assert_eq!(bytes, "123");
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
||||
to_bytes_limited(body, usize::MAX)
|
||||
.await
|
||||
.expect("body should never yield more than usize::MAX bytes")
|
||||
}
|
||||
|
||||
/// Error type returned from [`to_bytes_limited`] when body produced exceeds limit.
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(fmt = "limit exceeded while collecting body bytes")]
|
||||
#[non_exhaustive]
|
||||
pub struct BodyLimitExceeded;
|
||||
|
||||
/// Collects the bytes produced by `body`, up to `limit` bytes.
|
||||
///
|
||||
/// If a chunk read from `poll_next` causes the total number of bytes read to exceed `limit`, an
|
||||
/// `Err(BodyLimitExceeded)` is returned.
|
||||
///
|
||||
/// Any errors produced by the body stream are returned immediately as `Ok(Err(B::Error))`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use actix_http::body::{self, to_bytes_limited};
|
||||
/// use bytes::Bytes;
|
||||
///
|
||||
/// # actix_rt::System::new().block_on(async {
|
||||
/// let body = body::None::new();
|
||||
/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap();
|
||||
/// assert!(bytes.is_empty());
|
||||
///
|
||||
/// let body = Bytes::from_static(b"123");
|
||||
/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap();
|
||||
/// assert_eq!(bytes, "123");
|
||||
///
|
||||
/// let body = Bytes::from_static(b"123");
|
||||
/// assert!(to_bytes_limited(body, 2).await.is_err());
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn to_bytes_limited<B: MessageBody>(
|
||||
body: B,
|
||||
limit: usize,
|
||||
) -> Result<Result<Bytes, B::Error>, BodyLimitExceeded> {
|
||||
/// Sensible default (32kB) for initial, bounded allocation when collecting body bytes.
|
||||
const INITIAL_ALLOC_BYTES: usize = 32 * 1024;
|
||||
|
||||
let cap = match body.size() {
|
||||
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
|
||||
BodySize::Sized(size) => size as usize,
|
||||
// good enough first guess for chunk size
|
||||
BodySize::Stream => 32_768,
|
||||
BodySize::None | BodySize::Sized(0) => return Ok(Ok(Bytes::new())),
|
||||
BodySize::Sized(size) if size as usize > limit => return Err(BodyLimitExceeded),
|
||||
BodySize::Sized(size) => (size as usize).min(INITIAL_ALLOC_BYTES),
|
||||
BodySize::Stream => INITIAL_ALLOC_BYTES,
|
||||
};
|
||||
|
||||
let mut exceeded_limit = false;
|
||||
let mut buf = BytesMut::with_capacity(cap);
|
||||
|
||||
pin!(body);
|
||||
|
||||
poll_fn(|cx| loop {
|
||||
match poll_fn(|cx| loop {
|
||||
let body = body.as_mut();
|
||||
|
||||
match ready!(body.poll_next(cx)) {
|
||||
Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
|
||||
Some(Ok(bytes)) => {
|
||||
// if limit is exceeded...
|
||||
if buf.len() + bytes.len() > limit {
|
||||
// ...set flag to true and break out of poll_fn
|
||||
exceeded_limit = true;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
buf.extend_from_slice(&bytes)
|
||||
}
|
||||
None => return Poll::Ready(Ok(())),
|
||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
// propagate error returned from body poll
|
||||
Err(err) => Ok(Err(err)),
|
||||
|
||||
Ok(buf.freeze())
|
||||
// limit was exceeded while reading body
|
||||
Ok(()) if exceeded_limit => Err(BodyLimitExceeded),
|
||||
|
||||
// otherwise return body buffer
|
||||
Ok(()) => Ok(Ok(buf.freeze())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use std::io;
|
||||
|
||||
use futures_util::{stream, StreamExt as _};
|
||||
|
||||
use super::*;
|
||||
use crate::{body::BodyStream, Error};
|
||||
use crate::{
|
||||
body::{BodyStream, SizedStream},
|
||||
Error,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_to_bytes() {
|
||||
async fn to_bytes_complete() {
|
||||
let bytes = to_bytes(()).await.unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
|
||||
let body = Bytes::from_static(b"123");
|
||||
let bytes = to_bytes(body).await.unwrap();
|
||||
assert_eq!(bytes, b"123"[..]);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn to_bytes_streams() {
|
||||
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
|
||||
.map(Ok::<_, Error>);
|
||||
let body = BodyStream::new(stream);
|
||||
let bytes = to_bytes(body).await.unwrap();
|
||||
assert_eq!(bytes, b"123abc"[..]);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn to_bytes_limited_complete() {
|
||||
let bytes = to_bytes_limited((), 0).await.unwrap().unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
|
||||
let bytes = to_bytes_limited((), 1).await.unwrap().unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
|
||||
assert!(to_bytes_limited(Bytes::from_static(b"12"), 0)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(to_bytes_limited(Bytes::from_static(b"12"), 1)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(to_bytes_limited(Bytes::from_static(b"12"), 2).await.is_ok());
|
||||
assert!(to_bytes_limited(Bytes::from_static(b"12"), 3).await.is_ok());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn to_bytes_limited_streams() {
|
||||
// hinting a larger body fails
|
||||
let body = SizedStream::new(8, stream::empty().map(Ok::<_, Error>));
|
||||
assert!(to_bytes_limited(body, 3).await.is_err());
|
||||
|
||||
// hinting a smaller body is okay
|
||||
let body = SizedStream::new(3, stream::empty().map(Ok::<_, Error>));
|
||||
assert!(to_bytes_limited(body, 3).await.unwrap().unwrap().is_empty());
|
||||
|
||||
// hinting a smaller body then returning a larger one fails
|
||||
let stream = stream::iter(vec![Bytes::from_static(b"1234")]).map(Ok::<_, Error>);
|
||||
let body = SizedStream::new(3, stream);
|
||||
assert!(to_bytes_limited(body, 3).await.is_err());
|
||||
|
||||
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
|
||||
.map(Ok::<_, Error>);
|
||||
let body = BodyStream::new(stream);
|
||||
assert!(to_bytes_limited(body, 3).await.is_err());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn to_body_limit_error() {
|
||||
let err_stream = stream::once(async { Err(io::Error::new(io::ErrorKind::Other, "")) });
|
||||
let body = SizedStream::new(8, err_stream);
|
||||
// not too big, but propagates error from body stream
|
||||
assert!(to_bytes_limited(body, 10).await.unwrap().is_err());
|
||||
}
|
||||
}
|
||||
|
@@ -211,7 +211,6 @@ where
|
||||
|
||||
/// Finish service configuration and create a service for the HTTP/2 protocol.
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||
where
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
|
@@ -161,44 +161,44 @@ impl From<crate::ws::ProtocolError> for Error {
|
||||
#[non_exhaustive]
|
||||
pub enum ParseError {
|
||||
/// An invalid `Method`, such as `GE.T`.
|
||||
#[display(fmt = "Invalid Method specified")]
|
||||
#[display(fmt = "invalid method specified")]
|
||||
Method,
|
||||
|
||||
/// An invalid `Uri`, such as `exam ple.domain`.
|
||||
#[display(fmt = "Uri error: {}", _0)]
|
||||
#[display(fmt = "URI error: {}", _0)]
|
||||
Uri(InvalidUri),
|
||||
|
||||
/// An invalid `HttpVersion`, such as `HTP/1.1`
|
||||
#[display(fmt = "Invalid HTTP version specified")]
|
||||
#[display(fmt = "invalid HTTP version specified")]
|
||||
Version,
|
||||
|
||||
/// An invalid `Header`.
|
||||
#[display(fmt = "Invalid Header provided")]
|
||||
#[display(fmt = "invalid Header provided")]
|
||||
Header,
|
||||
|
||||
/// A message head is too large to be reasonable.
|
||||
#[display(fmt = "Message head is too large")]
|
||||
#[display(fmt = "message head is too large")]
|
||||
TooLarge,
|
||||
|
||||
/// A message reached EOF, but is not complete.
|
||||
#[display(fmt = "Message is incomplete")]
|
||||
#[display(fmt = "message is incomplete")]
|
||||
Incomplete,
|
||||
|
||||
/// An invalid `Status`, such as `1337 ELITE`.
|
||||
#[display(fmt = "Invalid Status provided")]
|
||||
#[display(fmt = "invalid status provided")]
|
||||
Status,
|
||||
|
||||
/// A timeout occurred waiting for an IO event.
|
||||
#[allow(dead_code)]
|
||||
#[display(fmt = "Timeout")]
|
||||
#[display(fmt = "timeout")]
|
||||
Timeout,
|
||||
|
||||
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
||||
#[display(fmt = "IO error: {}", _0)]
|
||||
/// An I/O error that occurred while trying to read or write to a network stream.
|
||||
#[display(fmt = "I/O error: {}", _0)]
|
||||
Io(io::Error),
|
||||
|
||||
/// Parsing a field as string failed.
|
||||
#[display(fmt = "UTF8 error: {}", _0)]
|
||||
#[display(fmt = "UTF-8 error: {}", _0)]
|
||||
Utf8(Utf8Error),
|
||||
}
|
||||
|
||||
@@ -257,22 +257,19 @@ impl From<ParseError> for Response<BoxBody> {
|
||||
#[non_exhaustive]
|
||||
pub enum PayloadError {
|
||||
/// A payload reached EOF, but is not complete.
|
||||
#[display(
|
||||
fmt = "A payload reached EOF, but is not complete. Inner error: {:?}",
|
||||
_0
|
||||
)]
|
||||
#[display(fmt = "payload reached EOF before completing: {:?}", _0)]
|
||||
Incomplete(Option<io::Error>),
|
||||
|
||||
/// Content encoding stream corruption.
|
||||
#[display(fmt = "Can not decode content-encoding.")]
|
||||
#[display(fmt = "can not decode content-encoding")]
|
||||
EncodingCorrupted,
|
||||
|
||||
/// Payload reached size limit.
|
||||
#[display(fmt = "Payload reached size limit.")]
|
||||
#[display(fmt = "payload reached size limit")]
|
||||
Overflow,
|
||||
|
||||
/// Payload length is unknown.
|
||||
#[display(fmt = "Payload length is unknown.")]
|
||||
#[display(fmt = "payload length is unknown")]
|
||||
UnknownLength,
|
||||
|
||||
/// HTTP/2 payload error.
|
||||
@@ -294,7 +291,6 @@ impl std::error::Error for PayloadError {
|
||||
PayloadError::Overflow => None,
|
||||
PayloadError::UnknownLength => None,
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
PayloadError::Http2Payload(err) => Some(err),
|
||||
PayloadError::Io(err) => Some(err),
|
||||
}
|
||||
@@ -331,44 +327,44 @@ impl From<PayloadError> for Error {
|
||||
#[non_exhaustive]
|
||||
pub enum DispatchError {
|
||||
/// Service error.
|
||||
#[display(fmt = "Service Error")]
|
||||
#[display(fmt = "service error")]
|
||||
Service(Response<BoxBody>),
|
||||
|
||||
/// Body streaming error.
|
||||
#[display(fmt = "Body error: {}", _0)]
|
||||
#[display(fmt = "body error: {}", _0)]
|
||||
Body(Box<dyn StdError>),
|
||||
|
||||
/// Upgrade service error.
|
||||
#[display(fmt = "upgrade error")]
|
||||
Upgrade,
|
||||
|
||||
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
||||
#[display(fmt = "IO error: {}", _0)]
|
||||
#[display(fmt = "I/O error: {}", _0)]
|
||||
Io(io::Error),
|
||||
|
||||
/// Request parse error.
|
||||
#[display(fmt = "Request parse error: {}", _0)]
|
||||
#[display(fmt = "request parse error: {}", _0)]
|
||||
Parse(ParseError),
|
||||
|
||||
/// HTTP/2 error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
H2(h2::Error),
|
||||
|
||||
/// The first request did not complete within the specified timeout.
|
||||
#[display(fmt = "The first request did not complete within the specified timeout")]
|
||||
#[display(fmt = "request did not complete within the specified timeout")]
|
||||
SlowRequestTimeout,
|
||||
|
||||
/// Disconnect timeout. Makes sense for ssl streams.
|
||||
#[display(fmt = "Connection shutdown timeout")]
|
||||
/// Disconnect timeout. Makes sense for TLS streams.
|
||||
#[display(fmt = "connection shutdown timeout")]
|
||||
DisconnectTimeout,
|
||||
|
||||
/// Handler dropped payload before reading EOF.
|
||||
#[display(fmt = "Handler dropped payload before reading EOF")]
|
||||
#[display(fmt = "handler dropped payload before reading EOF")]
|
||||
HandlerDroppedPayload,
|
||||
|
||||
/// Internal error.
|
||||
#[display(fmt = "Internal error")]
|
||||
#[display(fmt = "internal error")]
|
||||
InternalError,
|
||||
}
|
||||
|
||||
@@ -393,12 +389,12 @@ impl StdError for DispatchError {
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[non_exhaustive]
|
||||
pub enum ContentTypeError {
|
||||
/// Can not parse content type
|
||||
#[display(fmt = "Can not parse content type")]
|
||||
/// Can not parse content type.
|
||||
#[display(fmt = "could not parse content type")]
|
||||
ParseError,
|
||||
|
||||
/// Unknown content encoding
|
||||
#[display(fmt = "Unknown content encoding")]
|
||||
/// Unknown content encoding.
|
||||
#[display(fmt = "unknown content encoding")]
|
||||
UnknownEncoding,
|
||||
}
|
||||
|
||||
@@ -426,7 +422,7 @@ mod tests {
|
||||
let err: Error = ParseError::Io(orig).into();
|
||||
assert_eq!(
|
||||
format!("{}", err),
|
||||
"error parsing HTTP message: IO error: other"
|
||||
"error parsing HTTP message: I/O error: other"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -453,7 +449,7 @@ mod tests {
|
||||
let err = PayloadError::Incomplete(None);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"A payload reached EOF, but is not complete. Inner error: None"
|
||||
"payload reached EOF before completing: None"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -473,7 +469,7 @@ mod tests {
|
||||
match ParseError::from($from) {
|
||||
e @ $error => {
|
||||
let desc = format!("{}", e);
|
||||
assert_eq!(desc, format!("IO error: {}", $from));
|
||||
assert_eq!(desc, format!("I/O error: {}", $from));
|
||||
}
|
||||
_ => unreachable!("{:?}", $from),
|
||||
}
|
||||
|
@@ -134,7 +134,6 @@ mod openssl {
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create OpenSSL based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
@@ -197,7 +196,6 @@ mod rustls {
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create Rustls based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn rustls(
|
||||
self,
|
||||
config: ServerConfig,
|
||||
|
@@ -117,7 +117,6 @@ mod openssl {
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create OpenSSL based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
@@ -165,7 +164,6 @@ mod rustls {
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create Rustls based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn rustls(
|
||||
self,
|
||||
mut config: ServerConfig,
|
||||
|
@@ -8,12 +8,14 @@ use http::header::HeaderName;
|
||||
/// request.
|
||||
///
|
||||
/// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics.
|
||||
// TODO(breaking): replace with http's version
|
||||
pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status");
|
||||
|
||||
/// Response header field that allows origin servers to control the behavior of CDN caches
|
||||
/// interposed between them and clients separately from other caches that might handle the response.
|
||||
///
|
||||
/// See [RFC 9213](https://www.rfc-editor.org/rfc/rfc9213) for full semantics.
|
||||
// TODO(breaking): replace with http's version
|
||||
pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control");
|
||||
|
||||
/// Response header that prevents a document from loading any cross-origin resources that don't
|
||||
|
@@ -26,7 +26,7 @@
|
||||
)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
pub use ::http::{uri, uri::Uri};
|
||||
pub use ::http::{Method, StatusCode, Version};
|
||||
@@ -41,7 +41,6 @@ pub mod error;
|
||||
mod extensions;
|
||||
pub mod h1;
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
pub mod h2;
|
||||
pub mod header;
|
||||
mod helpers;
|
||||
@@ -56,7 +55,6 @@ mod responses;
|
||||
mod service;
|
||||
pub mod test;
|
||||
#[cfg(feature = "ws")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
|
||||
pub mod ws;
|
||||
|
||||
pub use self::builder::HttpServiceBuilder;
|
||||
@@ -74,7 +72,6 @@ pub use self::requests::{Request, RequestHead, RequestHeadType};
|
||||
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
||||
pub use self::service::HttpService;
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
|
||||
pub use self::service::TlsAcceptorConfig;
|
||||
|
||||
/// A major HTTP protocol version.
|
||||
|
@@ -217,7 +217,6 @@ where
|
||||
/// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2
|
||||
/// on plaintext connections.
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
pub fn tcp_auto_h2c(
|
||||
self,
|
||||
) -> impl ServiceFactory<
|
||||
@@ -253,7 +252,6 @@ where
|
||||
|
||||
/// Configuration options used when accepting TLS connection.
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TlsAcceptorConfig {
|
||||
pub(crate) handshake_timeout: Option<std::time::Duration>,
|
||||
@@ -309,7 +307,6 @@ mod openssl {
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create OpenSSL based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
@@ -324,7 +321,6 @@ mod openssl {
|
||||
}
|
||||
|
||||
/// Create OpenSSL based service with custom TLS acceptor configuration.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn openssl_with_config(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
@@ -404,7 +400,6 @@ mod rustls {
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create Rustls based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn rustls(
|
||||
self,
|
||||
config: ServerConfig,
|
||||
@@ -419,7 +414,6 @@ mod rustls {
|
||||
}
|
||||
|
||||
/// Create Rustls based service with custom TLS acceptor configuration.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn rustls_with_config(
|
||||
self,
|
||||
mut config: ServerConfig,
|
||||
|
@@ -26,39 +26,39 @@ pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode};
|
||||
#[derive(Debug, Display, Error, From)]
|
||||
pub enum ProtocolError {
|
||||
/// Received an unmasked frame from client.
|
||||
#[display(fmt = "Received an unmasked frame from client.")]
|
||||
#[display(fmt = "received an unmasked frame from client")]
|
||||
UnmaskedFrame,
|
||||
|
||||
/// Received a masked frame from server.
|
||||
#[display(fmt = "Received a masked frame from server.")]
|
||||
#[display(fmt = "received a masked frame from server")]
|
||||
MaskedFrame,
|
||||
|
||||
/// Encountered invalid opcode.
|
||||
#[display(fmt = "Invalid opcode: {}.", _0)]
|
||||
#[display(fmt = "invalid opcode ({})", _0)]
|
||||
InvalidOpcode(#[error(not(source))] u8),
|
||||
|
||||
/// Invalid control frame length
|
||||
#[display(fmt = "Invalid control frame length: {}.", _0)]
|
||||
#[display(fmt = "invalid control frame length ({})", _0)]
|
||||
InvalidLength(#[error(not(source))] usize),
|
||||
|
||||
/// Bad opcode.
|
||||
#[display(fmt = "Bad opcode.")]
|
||||
#[display(fmt = "bad opcode")]
|
||||
BadOpCode,
|
||||
|
||||
/// A payload reached size limit.
|
||||
#[display(fmt = "A payload reached size limit.")]
|
||||
#[display(fmt = "payload reached size limit")]
|
||||
Overflow,
|
||||
|
||||
/// Continuation is not started.
|
||||
#[display(fmt = "Continuation is not started.")]
|
||||
/// Continuation has not started.
|
||||
#[display(fmt = "continuation has not started")]
|
||||
ContinuationNotStarted,
|
||||
|
||||
/// Received new continuation but it is already started.
|
||||
#[display(fmt = "Received new continuation but it is already started.")]
|
||||
#[display(fmt = "received new continuation but it has already started")]
|
||||
ContinuationStarted,
|
||||
|
||||
/// Unknown continuation fragment.
|
||||
#[display(fmt = "Unknown continuation fragment: {}.", _0)]
|
||||
#[display(fmt = "unknown continuation fragment: {}", _0)]
|
||||
ContinuationFragment(#[error(not(source))] OpCode),
|
||||
|
||||
/// I/O error.
|
||||
@@ -70,27 +70,27 @@ pub enum ProtocolError {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
|
||||
pub enum HandshakeError {
|
||||
/// Only get method is allowed.
|
||||
#[display(fmt = "Method not allowed.")]
|
||||
#[display(fmt = "method not allowed")]
|
||||
GetMethodRequired,
|
||||
|
||||
/// Upgrade header if not set to WebSocket.
|
||||
#[display(fmt = "WebSocket upgrade is expected.")]
|
||||
#[display(fmt = "WebSocket upgrade is expected")]
|
||||
NoWebsocketUpgrade,
|
||||
|
||||
/// Connection header is not set to upgrade.
|
||||
#[display(fmt = "Connection upgrade is expected.")]
|
||||
#[display(fmt = "connection upgrade is expected")]
|
||||
NoConnectionUpgrade,
|
||||
|
||||
/// WebSocket version header is not set.
|
||||
#[display(fmt = "WebSocket version header is required.")]
|
||||
#[display(fmt = "WebSocket version header is required")]
|
||||
NoVersionHeader,
|
||||
|
||||
/// Unsupported WebSocket version.
|
||||
#[display(fmt = "Unsupported WebSocket version.")]
|
||||
#[display(fmt = "unsupported WebSocket version")]
|
||||
UnsupportedVersion,
|
||||
|
||||
/// WebSocket key is not set or wrong.
|
||||
#[display(fmt = "Unknown websocket key.")]
|
||||
#[display(fmt = "unknown WebSocket key")]
|
||||
BadWebsocketKey,
|
||||
}
|
||||
|
||||
|
@@ -39,13 +39,13 @@ impl WsService {
|
||||
|
||||
#[derive(Debug, Display, Error, From)]
|
||||
enum WsServiceError {
|
||||
#[display(fmt = "http error")]
|
||||
#[display(fmt = "HTTP error")]
|
||||
Http(actix_http::Error),
|
||||
|
||||
#[display(fmt = "ws handshake error")]
|
||||
#[display(fmt = "WS handshake error")]
|
||||
Ws(actix_http::ws::HandshakeError),
|
||||
|
||||
#[display(fmt = "io error")]
|
||||
#[display(fmt = "I/O error")]
|
||||
Io(std::io::Error),
|
||||
|
||||
#[display(fmt = "dispatcher error")]
|
||||
|
5
actix-multipart-derive/CHANGES.md
Normal file
5
actix-multipart-derive/CHANGES.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Changes
|
||||
|
||||
## 0.6.0 - 2023-02-26
|
||||
|
||||
- Add `MultipartForm` derive macro.
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-multipart-derive"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
authors = ["Jacob Halsey <jacob@jhalsey.com>"]
|
||||
description = "Multipart form derive macro for Actix Web"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@@ -9,6 +9,10 @@ repository = "https://github.com/actix/actix-web.git"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
all-features = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
@@ -20,7 +24,7 @@ quote = "1"
|
||||
syn = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-multipart = "0.5"
|
||||
actix-multipart = "0.6"
|
||||
actix-web = "4"
|
||||
rustversion = "1"
|
||||
trybuild = "1"
|
||||
|
@@ -1,3 +1,17 @@
|
||||
# actix-multipart-derive
|
||||
|
||||
> The derive macro implementation for actix-multipart.
|
||||
> The derive macro implementation for actix-multipart-derive.
|
||||
|
||||
[](https://crates.io/crates/actix-multipart-derive)
|
||||
[](https://docs.rs/actix-multipart-derive/0.5.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart-derive/0.5.0)
|
||||
[](https://crates.io/crates/actix-multipart-derive)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-multipart-derive)
|
||||
- Minimum Supported Rust Version (MSRV): 1.59
|
||||
|
@@ -6,7 +6,7 @@
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
use std::{collections::HashSet, convert::TryFrom as _};
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
## Unreleased - 2023-xx-xx
|
||||
|
||||
## 0.6.0 - 2023-02-26
|
||||
|
||||
- Added `MultipartForm` typed data extractor. [#2883]
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Jacob Halsey <jacob@jhalsey.com>",
|
||||
@@ -21,12 +21,8 @@ default = ["tempfile", "derive"]
|
||||
derive = ["actix-multipart-derive"]
|
||||
tempfile = ["tempfile-dep", "tokio/fs"]
|
||||
|
||||
[lib]
|
||||
name = "actix_multipart"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-multipart-derive = { version = "=0.5.0", optional = true }
|
||||
actix-multipart-derive = { version = "=0.6.0", optional = true }
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4", default-features = false }
|
||||
|
||||
@@ -44,7 +40,7 @@ serde_json = "1"
|
||||
serde_plain = "1"
|
||||
# TODO(MSRV 1.60): replace with dep: prefix
|
||||
tempfile-dep = { package = "tempfile", version = "3.4", optional = true }
|
||||
tokio = { version = "1.18.5", features = ["sync"] }
|
||||
tokio = { version = "1.24.2", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http = "3"
|
||||
@@ -53,5 +49,5 @@ actix-rt = "2.2"
|
||||
actix-test = "0.1"
|
||||
awc = "3"
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
tokio = { version = "1.18.5", features = ["sync"] }
|
||||
tokio = { version = "1.24.2", features = ["sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
@@ -3,11 +3,11 @@
|
||||
> Multipart form support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://docs.rs/actix-multipart/0.5.0)
|
||||
[](https://docs.rs/actix-multipart/0.6.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart/0.5.0)
|
||||
[](https://deps.rs/crate/actix-multipart/0.6.0)
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@@ -16,12 +16,10 @@ use crate::{Field, Multipart, MultipartError};
|
||||
|
||||
pub mod bytes;
|
||||
pub mod json;
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tempfile")))]
|
||||
#[cfg(feature = "tempfile")]
|
||||
pub mod tempfile;
|
||||
pub mod text;
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
|
||||
#[cfg(feature = "derive")]
|
||||
pub use actix_multipart_derive::MultipartForm;
|
||||
|
||||
|
@@ -3,7 +3,9 @@
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::borrow_interior_mutable_const, clippy::uninlined_format_args)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
// This allows us to use the actix_multipart_derive within this crate's tests
|
||||
#[cfg(test)]
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
## Unreleased - 2023-xx-xx
|
||||
|
||||
## 0.5.1 - 2022-09-19
|
||||
|
||||
|
@@ -21,14 +21,14 @@ default = ["http"]
|
||||
|
||||
[dependencies]
|
||||
bytestring = ">=0.1.5, <2"
|
||||
http = { version = "0.2.5", optional = true }
|
||||
http = { version = "0.2.7", optional = true }
|
||||
regex = "1.5"
|
||||
serde = "1"
|
||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.4", features = ["html_reports"] }
|
||||
http = "0.2.5"
|
||||
http = "0.2.7"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
percent-encoding = "2.1"
|
||||
|
||||
|
@@ -5,6 +5,7 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
mod de;
|
||||
mod path;
|
||||
|
@@ -1,7 +1,10 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
## Unreleased - 2023-xx-xx
|
||||
|
||||
## 0.1.1 - 2023-02-26
|
||||
|
||||
- Add `TestServerConfig::port()` setter method.
|
||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||
|
||||
## 0.1.0 - 2022-07-24
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-test"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@@ -45,4 +45,4 @@ serde_json = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
||||
tokio = { version = "1.18.5", features = ["sync"] }
|
||||
tokio = { version = "1.24.2", features = ["sync"] }
|
||||
|
@@ -28,6 +28,9 @@
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
extern crate tls_openssl as openssl;
|
||||
@@ -145,7 +148,7 @@ where
|
||||
// run server in separate orphaned thread
|
||||
thread::spawn(move || {
|
||||
rt::System::new().block_on(async move {
|
||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let tcp = net::TcpListener::bind(("127.0.0.1", cfg.port)).unwrap();
|
||||
let local_addr = tcp.local_addr().unwrap();
|
||||
let factory = factory.clone();
|
||||
let srv_cfg = cfg.clone();
|
||||
@@ -390,6 +393,7 @@ pub struct TestServerConfig {
|
||||
tp: HttpVer,
|
||||
stream: StreamType,
|
||||
client_request_timeout: Duration,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl Default for TestServerConfig {
|
||||
@@ -405,6 +409,7 @@ impl TestServerConfig {
|
||||
tp: HttpVer::Both,
|
||||
stream: StreamType::Tcp,
|
||||
client_request_timeout: Duration::from_secs(5),
|
||||
port: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,6 +444,14 @@ impl TestServerConfig {
|
||||
self.client_request_timeout = dur;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets test server port.
|
||||
///
|
||||
/// By default, a random free port is determined by the OS.
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A basic HTTP server controller that simplifies the process of writing integration tests for
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
## Unreleased - 2023-xx-xx
|
||||
|
||||
## 4.2.0 - 2023-01-21
|
||||
|
||||
|
@@ -23,7 +23,7 @@ bytes = "1"
|
||||
bytestring = "1"
|
||||
futures-core = { version = "0.3.17", default-features = false }
|
||||
pin-project-lite = "0.2"
|
||||
tokio = { version = "1.18.5", features = ["sync"] }
|
||||
tokio = { version = "1.24.2", features = ["sync"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@@ -58,6 +58,9 @@
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
mod context;
|
||||
pub mod ws;
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
## Unreleased - 2023-xx-xx
|
||||
|
||||
## 4.2.0 - 2023-02-26
|
||||
|
||||
- Add support for Custom Methods with `#[route]` macro. [#2969]
|
||||
- Add support for custom methods with the `#[route]` macro. [#2969]
|
||||
|
||||
[#2969]: https://github.com/actix/actix-web/pull/2969
|
||||
|
||||
|
@@ -75,6 +75,9 @@
|
||||
#![recursion_limit = "512"]
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
|
@@ -1,6 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
## Unreleased - 2023-xx-xx
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Resource::{get, post, etc...}` methods for more concisely adding routes that don't need additional guards.
|
||||
- Add `Compress::with_predicate()` method for customizing when compression is applied.
|
||||
|
||||
### Changed
|
||||
|
||||
- Handler functions can now receive up to 16 extractor parameters.
|
||||
- The `Compress` no longer compresses image or video content by default.
|
||||
|
||||
## 4.3.1 - 2023-02-26
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for custom methods with the `#[route]` macro. [#2969]
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.3.0"
|
||||
version = "4.3.1"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@@ -72,7 +72,7 @@ actix-http = { version = "3.3", features = ["http2", "ws"] }
|
||||
actix-router = "0.5"
|
||||
actix-web-codegen = { version = "4.2", optional = true }
|
||||
|
||||
ahash = "0.7"
|
||||
ahash = "0.8"
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
cfg-if = "1"
|
||||
@@ -81,7 +81,6 @@ derive_more = "0.99.8"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.17", default-features = false }
|
||||
futures-util = { version = "0.3.17", default-features = false }
|
||||
http = "0.2.8"
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
log = "0.4"
|
||||
@@ -93,7 +92,7 @@ serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7"
|
||||
smallvec = "1.6.1"
|
||||
socket2 = "0.4.0"
|
||||
socket2 = "0.4"
|
||||
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||
url = "2.1"
|
||||
|
||||
@@ -115,7 +114,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
tokio = { version = "1.18.5", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.12"
|
||||
|
||||
[[test]]
|
||||
|
@@ -5,7 +5,7 @@
|
||||
</p>
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web) [](https://docs.rs/actix-web/4.3.0)   [](https://deps.rs/crate/actix-web/4.3.0) <br /> [](https://github.com/actix/actix-web/actions/workflows/ci.yml) [](https://codecov.io/gh/actix/actix-web)  [](https://discord.gg/NWpN5mmg3x)
|
||||
[](https://crates.io/crates/actix-web) [](https://docs.rs/actix-web/4.3.1)   [](https://deps.rs/crate/actix-web/4.3.1) <br /> [](https://github.com/actix/actix-web/actions/workflows/ci.yml) [](https://codecov.io/gh/actix/actix-web)  [](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
</p>
|
||||
</div>
|
||||
|
@@ -152,7 +152,7 @@ mod tests {
|
||||
let resp_err: &dyn ResponseError = &err;
|
||||
|
||||
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
|
||||
assert_eq!(err.to_string(), "Payload reached size limit.");
|
||||
assert_eq!(err.to_string(), "payload reached size limit");
|
||||
|
||||
let not_err = resp_err.downcast_ref::<ContentTypeError>();
|
||||
assert!(not_err.is_none());
|
||||
|
@@ -416,6 +416,10 @@ mod tuple_from_req {
|
||||
tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J }
|
||||
tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K }
|
||||
tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L }
|
||||
tuple_from_req! { TupleFromRequest13; A, B, C, D, E, F, G, H, I, J, K, L, M }
|
||||
tuple_from_req! { TupleFromRequest14; A, B, C, D, E, F, G, H, I, J, K, L, M, N }
|
||||
tuple_from_req! { TupleFromRequest15; A, B, C, D, E, F, G, H, I, J, K, L, M, N, O }
|
||||
tuple_from_req! { TupleFromRequest16; A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@@ -151,6 +151,10 @@ factory_tuple! { A B C D E F G H I }
|
||||
factory_tuple! { A B C D E F G H I J }
|
||||
factory_tuple! { A B C D E F G H I J K }
|
||||
factory_tuple! { A B C D E F G H I J K L }
|
||||
factory_tuple! { A B C D E F G H I J K L M }
|
||||
factory_tuple! { A B C D E F G H I J K L M N }
|
||||
factory_tuple! { A B C D E F G H I J K L M N O }
|
||||
factory_tuple! { A B C D E F G H I J K L M N O P }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -167,6 +171,7 @@ mod tests {
|
||||
async fn handler_max(
|
||||
_01: (), _02: (), _03: (), _04: (), _05: (), _06: (),
|
||||
_07: (), _08: (), _09: (), _10: (), _11: (), _12: (),
|
||||
_13: (), _14: (), _15: (), _16: (),
|
||||
) {}
|
||||
|
||||
assert_impl_handler(handler_min);
|
||||
|
@@ -1,110 +1,104 @@
|
||||
use super::CONTENT_TYPE;
|
||||
use mime::Mime;
|
||||
|
||||
use super::CONTENT_TYPE;
|
||||
|
||||
crate::http::header::common_header! {
|
||||
/// `Content-Type` header, defined
|
||||
/// in [RFC 7231 §3.1.1.5](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5)
|
||||
/// `Content-Type` header, defined in [RFC 9110 §8.3].
|
||||
///
|
||||
/// The `Content-Type` header field indicates the media type of the
|
||||
/// associated representation: either the representation enclosed in the
|
||||
/// message payload or the selected representation, as determined by the
|
||||
/// message semantics. The indicated media type defines both the data
|
||||
/// format and how that data is intended to be processed by a recipient,
|
||||
/// within the scope of the received message semantics, after any content
|
||||
/// codings indicated by Content-Encoding are decoded.
|
||||
/// The `Content-Type` header field indicates the media type of the associated representation:
|
||||
/// either the representation enclosed in the message payload or the selected representation,
|
||||
/// as determined by the message semantics. The indicated media type defines both the data
|
||||
/// format and how that data is intended to be processed by a recipient, within the scope of the
|
||||
/// received message semantics, after any content codings indicated by Content-Encoding are
|
||||
/// decoded.
|
||||
///
|
||||
/// Although the `mime` crate allows the mime options to be any slice, this crate
|
||||
/// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If
|
||||
/// this is an issue, it's possible to implement `Header` on a custom struct.
|
||||
/// Although the `mime` crate allows the mime options to be any slice, this crate forces the use
|
||||
/// of Vec. This is to make sure the same header can't have more than 1 type. If this is an
|
||||
/// issue, it's possible to implement `Header` on a custom struct.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```plain
|
||||
/// Content-Type = media-type
|
||||
/// ```
|
||||
///
|
||||
/// # Example Values
|
||||
/// * `text/html; charset=utf-8`
|
||||
/// * `application/json`
|
||||
///
|
||||
/// - `text/html; charset=utf-8`
|
||||
/// - `application/json`
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::HttpResponse;
|
||||
/// use actix_web::http::header::ContentType;
|
||||
///
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// ContentType::json()
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::HttpResponse;
|
||||
/// use actix_web::http::header::ContentType;
|
||||
/// use actix_web::{http::header::ContentType, HttpResponse};
|
||||
///
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// ContentType(mime::TEXT_HTML)
|
||||
/// );
|
||||
/// let res_json = HttpResponse::Ok()
|
||||
/// .insert_header(ContentType::json());
|
||||
///
|
||||
/// let res_html = HttpResponse::Ok()
|
||||
/// .insert_header(ContentType(mime::TEXT_HTML));
|
||||
/// ```
|
||||
///
|
||||
/// [RFC 9110 §8.3]: https://datatracker.ietf.org/doc/html/rfc9110#section-8.3
|
||||
(ContentType, CONTENT_TYPE) => [Mime]
|
||||
|
||||
test_parse_and_format {
|
||||
crate::http::header::common_header_test!(
|
||||
test1,
|
||||
test_text_html,
|
||||
vec![b"text/html"],
|
||||
Some(HeaderField(mime::TEXT_HTML)));
|
||||
crate::http::header::common_header_test!(
|
||||
test_image_star,
|
||||
vec![b"image/*"],
|
||||
Some(HeaderField(mime::IMAGE_STAR)));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentType {
|
||||
/// A constructor to easily create a `Content-Type: application/json`
|
||||
/// header.
|
||||
/// Constructs a `Content-Type: application/json` header.
|
||||
#[inline]
|
||||
pub fn json() -> ContentType {
|
||||
ContentType(mime::APPLICATION_JSON)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: text/plain;
|
||||
/// charset=utf-8` header.
|
||||
/// Constructs a `Content-Type: text/plain; charset=utf-8` header.
|
||||
#[inline]
|
||||
pub fn plaintext() -> ContentType {
|
||||
ContentType(mime::TEXT_PLAIN_UTF_8)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: text/html; charset=utf-8`
|
||||
/// header.
|
||||
/// Constructs a `Content-Type: text/html; charset=utf-8` header.
|
||||
#[inline]
|
||||
pub fn html() -> ContentType {
|
||||
ContentType(mime::TEXT_HTML_UTF_8)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: text/xml` header.
|
||||
/// Constructs a `Content-Type: text/xml` header.
|
||||
#[inline]
|
||||
pub fn xml() -> ContentType {
|
||||
ContentType(mime::TEXT_XML)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type:
|
||||
/// application/www-form-url-encoded` header.
|
||||
/// Constructs a `Content-Type: application/www-form-url-encoded` header.
|
||||
#[inline]
|
||||
pub fn form_url_encoded() -> ContentType {
|
||||
ContentType(mime::APPLICATION_WWW_FORM_URLENCODED)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: image/jpeg` header.
|
||||
/// Constructs a `Content-Type: image/jpeg` header.
|
||||
#[inline]
|
||||
pub fn jpeg() -> ContentType {
|
||||
ContentType(mime::IMAGE_JPEG)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: image/png` header.
|
||||
/// Constructs a `Content-Type: image/png` header.
|
||||
#[inline]
|
||||
pub fn png() -> ContentType {
|
||||
ContentType(mime::IMAGE_PNG)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type:
|
||||
/// application/octet-stream` header.
|
||||
/// Constructs a `Content-Type: application/octet-stream` header.
|
||||
#[inline]
|
||||
pub fn octet_stream() -> ContentType {
|
||||
ContentType(mime::APPLICATION_OCTET_STREAM)
|
||||
|
@@ -72,7 +72,7 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
mod app;
|
||||
mod app_service;
|
||||
@@ -119,14 +119,12 @@ pub use crate::types::Either;
|
||||
pub use actix_http::{body, HttpMessage};
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "cookies")))]
|
||||
#[doc(inline)]
|
||||
pub use cookie;
|
||||
|
||||
macro_rules! codegen_reexport {
|
||||
($name:ident) => {
|
||||
#[cfg(feature = "macros")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
|
||||
pub use actix_web_codegen::$name;
|
||||
};
|
||||
}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
//! For middleware documentation, see [`Compress`].
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
@@ -11,19 +13,22 @@ use actix_http::encoding::Encoder;
|
||||
use actix_service::{Service, Transform};
|
||||
use actix_utils::future::{ok, Either, Ready};
|
||||
use futures_core::ready;
|
||||
use mime::Mime;
|
||||
use once_cell::sync::Lazy;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
body::{EitherBody, MessageBody},
|
||||
http::{
|
||||
header::{self, AcceptEncoding, Encoding, HeaderValue},
|
||||
header::{self, AcceptEncoding, ContentEncoding, Encoding, HeaderValue},
|
||||
StatusCode,
|
||||
},
|
||||
service::{ServiceRequest, ServiceResponse},
|
||||
Error, HttpMessage, HttpResponse,
|
||||
};
|
||||
|
||||
type CompressPredicateFn = Rc<dyn Fn(Option<&HeaderValue>) -> bool>;
|
||||
|
||||
/// Middleware for compressing response payloads.
|
||||
///
|
||||
/// # Encoding Negotiation
|
||||
@@ -71,9 +76,80 @@ use crate::{
|
||||
/// ```
|
||||
///
|
||||
/// [feature flags]: ../index.html#crate-features
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct Compress;
|
||||
pub struct Compress {
|
||||
predicate: CompressPredicateFn,
|
||||
}
|
||||
|
||||
impl Compress {
|
||||
/// Sets the `predicate` function to use when deciding if response should be compressed or not.
|
||||
///
|
||||
/// The `predicate` function receives the response's current `Content-Type` header, if set.
|
||||
/// Returning true from the predicate will instruct this middleware to compress the response.
|
||||
///
|
||||
/// By default, video and image responses are unaffected (since they are typically compressed
|
||||
/// already) and responses without a Content-Type header will be compressed. Custom predicate
|
||||
/// functions should try to maintain these default rules.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{App, middleware::Compress};
|
||||
///
|
||||
/// App::new()
|
||||
/// .wrap(Compress::default().with_predicate(|content_type| {
|
||||
/// // preserve that missing Content-Type header compresses content
|
||||
/// let ct = match content_type.and_then(|ct| ct.to_str().ok()) {
|
||||
/// None => return true,
|
||||
/// Some(ct) => ct,
|
||||
/// };
|
||||
///
|
||||
/// // parse Content-Type as MIME type
|
||||
/// let ct_mime = match ct.parse::<mime::Mime>() {
|
||||
/// Err(_) => return true,
|
||||
/// Ok(mime) => mime,
|
||||
/// };
|
||||
///
|
||||
/// // compress everything except HTML documents
|
||||
/// ct_mime.subtype() != mime::HTML
|
||||
/// }))
|
||||
/// # ;
|
||||
/// ```
|
||||
pub fn with_predicate(
|
||||
self,
|
||||
predicate: impl Fn(Option<&HeaderValue>) -> bool + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
predicate: Rc::new(predicate),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Compress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Compress").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Compress {
|
||||
fn default() -> Self {
|
||||
fn default_compress_predicate(content_type: Option<&HeaderValue>) -> bool {
|
||||
match content_type {
|
||||
None => true,
|
||||
Some(hdr) => match hdr.to_str().ok().and_then(|hdr| hdr.parse::<Mime>().ok()) {
|
||||
Some(mime) if mime.type_().as_str() == "image" => false,
|
||||
Some(mime) if mime.type_().as_str() == "video" => false,
|
||||
_ => true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Compress {
|
||||
predicate: Rc::new(default_compress_predicate),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> for Compress
|
||||
where
|
||||
@@ -87,12 +163,16 @@ where
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ok(CompressMiddleware { service })
|
||||
ok(CompressMiddleware {
|
||||
service,
|
||||
predicate: Rc::clone(&self.predicate),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompressMiddleware<S> {
|
||||
service: S,
|
||||
predicate: CompressPredicateFn,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
|
||||
@@ -118,6 +198,7 @@ where
|
||||
return Either::left(CompressResponse {
|
||||
encoding: Encoding::identity(),
|
||||
fut: self.service.call(req),
|
||||
predicate: Rc::clone(&self.predicate),
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
@@ -145,6 +226,7 @@ where
|
||||
Some(encoding) => Either::left(CompressResponse {
|
||||
fut: self.service.call(req),
|
||||
encoding,
|
||||
predicate: Rc::clone(&self.predicate),
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
}
|
||||
@@ -159,6 +241,7 @@ pin_project! {
|
||||
#[pin]
|
||||
fut: S::Future,
|
||||
encoding: Encoding,
|
||||
predicate: CompressPredicateFn,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
}
|
||||
@@ -170,19 +253,29 @@ where
|
||||
{
|
||||
type Output = Result<ServiceResponse<EitherBody<Encoder<B>>>, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.as_mut().project();
|
||||
|
||||
match ready!(this.fut.poll(cx)) {
|
||||
Ok(resp) => {
|
||||
let enc = match this.encoding {
|
||||
Encoding::Known(enc) => *enc,
|
||||
Encoding::Unknown(enc) => {
|
||||
unimplemented!("encoding {} should not be here", enc);
|
||||
unimplemented!("encoding '{enc}' should not be here");
|
||||
}
|
||||
};
|
||||
|
||||
Poll::Ready(Ok(resp.map_body(move |head, body| {
|
||||
let content_type = head.headers.get(header::CONTENT_TYPE);
|
||||
|
||||
let should_compress = (self.predicate)(content_type);
|
||||
|
||||
let enc = if should_compress {
|
||||
enc
|
||||
} else {
|
||||
ContentEncoding::Identity
|
||||
};
|
||||
|
||||
EitherBody::left(Encoder::response(enc, head, body))
|
||||
})))
|
||||
}
|
||||
@@ -246,9 +339,20 @@ static SUPPORTED_ENCODINGS: &[Encoding] = &[
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
// use static_assertions::assert_impl_all;
|
||||
|
||||
use super::*;
|
||||
use crate::http::header::ContentType;
|
||||
use crate::{middleware::DefaultHeaders, test, web, App};
|
||||
|
||||
const HTML_DATA_PART: &str = "<html><h1>hello world</h1></html";
|
||||
const HTML_DATA: &str = const_str::repeat!(HTML_DATA_PART, 100);
|
||||
|
||||
const TEXT_DATA_PART: &str = "hello world ";
|
||||
const TEXT_DATA: &str = const_str::repeat!(TEXT_DATA_PART, 100);
|
||||
|
||||
// assert_impl_all!(Compress: Send, Sync);
|
||||
|
||||
pub fn gzip_decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
use std::io::Read as _;
|
||||
let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref());
|
||||
@@ -257,23 +361,55 @@ mod tests {
|
||||
buf
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_successful_res_with_content_type<B>(res: &ServiceResponse<B>, ct: &str) {
|
||||
assert!(res.status().is_success());
|
||||
assert!(
|
||||
res.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.expect("content-type header should be present")
|
||||
.to_str()
|
||||
.expect("content-type header should be utf-8")
|
||||
.contains(ct),
|
||||
"response's content-type did not match {}",
|
||||
ct
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_successful_gzip_res_with_content_type<B>(res: &ServiceResponse<B>, ct: &str) {
|
||||
assert_successful_res_with_content_type(res, ct);
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get(header::CONTENT_ENCODING)
|
||||
.expect("response should be gzip compressed"),
|
||||
"gzip",
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_successful_identity_res_with_content_type<B>(res: &ServiceResponse<B>, ct: &str) {
|
||||
assert_successful_res_with_content_type(res, ct);
|
||||
assert!(
|
||||
res.headers().get(header::CONTENT_ENCODING).is_none(),
|
||||
"response should not be compressed",
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn prevents_double_compressing() {
|
||||
const D: &str = "hello world ";
|
||||
const DATA: &str = const_str::repeat!(D, 100);
|
||||
|
||||
let app = test::init_service({
|
||||
App::new()
|
||||
.wrap(Compress::default())
|
||||
.route(
|
||||
"/single",
|
||||
web::get().to(move || HttpResponse::Ok().body(DATA)),
|
||||
web::get().to(move || HttpResponse::Ok().body(TEXT_DATA)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/double")
|
||||
.wrap(Compress::default())
|
||||
.wrap(DefaultHeaders::new().add(("x-double", "true")))
|
||||
.route(web::get().to(move || HttpResponse::Ok().body(DATA))),
|
||||
.route(web::get().to(move || HttpResponse::Ok().body(TEXT_DATA))),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
@@ -287,7 +423,7 @@ mod tests {
|
||||
assert_eq!(res.headers().get("x-double"), None);
|
||||
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
|
||||
let bytes = test::read_body(res).await;
|
||||
assert_eq!(gzip_decode(bytes), DATA.as_bytes());
|
||||
assert_eq!(gzip_decode(bytes), TEXT_DATA.as_bytes());
|
||||
|
||||
let req = test::TestRequest::default()
|
||||
.uri("/double")
|
||||
@@ -298,7 +434,7 @@ mod tests {
|
||||
assert_eq!(res.headers().get("x-double").unwrap(), "true");
|
||||
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
|
||||
let bytes = test::read_body(res).await;
|
||||
assert_eq!(gzip_decode(bytes), DATA.as_bytes());
|
||||
assert_eq!(gzip_decode(bytes), TEXT_DATA.as_bytes());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -324,4 +460,81 @@ mod tests {
|
||||
assert!(vary_headers.contains(&HeaderValue::from_static("x-test")));
|
||||
assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding")));
|
||||
}
|
||||
|
||||
fn configure_predicate_test(cfg: &mut web::ServiceConfig) {
|
||||
cfg.route(
|
||||
"/html",
|
||||
web::get().to(|| {
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::html())
|
||||
.body(HTML_DATA)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/image",
|
||||
web::get().to(|| {
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::jpeg())
|
||||
.body(TEXT_DATA)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn prevents_compression_jpeg() {
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.wrap(Compress::default())
|
||||
.configure(configure_predicate_test),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = test::TestRequest::with_uri("/html")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"));
|
||||
let res = test::call_service(&app, req.to_request()).await;
|
||||
assert_successful_gzip_res_with_content_type(&res, "text/html");
|
||||
assert_ne!(test::read_body(res).await, HTML_DATA.as_bytes());
|
||||
|
||||
let req = test::TestRequest::with_uri("/image")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"));
|
||||
let res = test::call_service(&app, req.to_request()).await;
|
||||
assert_successful_identity_res_with_content_type(&res, "image/jpeg");
|
||||
assert_eq!(test::read_body(res).await, TEXT_DATA.as_bytes());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn prevents_compression_custom_predicate() {
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.wrap(Compress::default().with_predicate(|hdr| {
|
||||
// preserve that missing CT header compresses content
|
||||
let hdr = match hdr.and_then(|hdr| hdr.to_str().ok()) {
|
||||
None => return true,
|
||||
Some(hdr) => hdr,
|
||||
};
|
||||
|
||||
let mime = match hdr.parse::<mime::Mime>() {
|
||||
Err(_) => return true,
|
||||
Ok(mime) => mime,
|
||||
};
|
||||
|
||||
// compress everything except HTML documents
|
||||
mime.subtype() != mime::HTML
|
||||
}))
|
||||
.configure(configure_predicate_test),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = test::TestRequest::with_uri("/html")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"));
|
||||
let res = test::call_service(&app, req.to_request()).await;
|
||||
assert_successful_identity_res_with_content_type(&res, "text/html");
|
||||
assert_eq!(test::read_body(res).await, HTML_DATA.as_bytes());
|
||||
|
||||
let req = test::TestRequest::with_uri("/image")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"));
|
||||
let res = test::call_service(&app, req.to_request()).await;
|
||||
assert_successful_gzip_res_with_content_type(&res, "image/jpeg");
|
||||
assert_ne!(test::read_body(res).await, TEXT_DATA.as_bytes());
|
||||
}
|
||||
}
|
||||
|
@@ -50,18 +50,24 @@ type DefaultHandler<B> = Option<Rc<ErrorHandler<B>>>;
|
||||
/// will pass by unchanged by this middleware.
|
||||
///
|
||||
/// # Examples
|
||||
/// ## Handler Response
|
||||
/// Header
|
||||
/// ```
|
||||
/// use actix_web::http::{header, StatusCode};
|
||||
/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
|
||||
/// use actix_web::{dev, web, App, HttpResponse, Result};
|
||||
///
|
||||
/// fn add_error_header<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// Adding a header:
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{
|
||||
/// dev::ServiceResponse,
|
||||
/// http::{header, StatusCode},
|
||||
/// middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||
/// web, App, HttpResponse, Result,
|
||||
/// };
|
||||
///
|
||||
/// fn add_error_header<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// res.response_mut().headers_mut().insert(
|
||||
/// header::CONTENT_TYPE,
|
||||
/// header::HeaderValue::from_static("Error"),
|
||||
/// );
|
||||
///
|
||||
/// // body is unchanged, map to "left" slot
|
||||
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
/// }
|
||||
///
|
||||
@@ -70,45 +76,62 @@ type DefaultHandler<B> = Option<Rc<ErrorHandler<B>>>;
|
||||
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
|
||||
/// ```
|
||||
///
|
||||
/// Body Content
|
||||
/// Modifying response body:
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::http::{header, StatusCode};
|
||||
/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
|
||||
/// use actix_web::{dev, web, App, HttpResponse, Result};
|
||||
/// fn add_error_body<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// // Get the error message and status code
|
||||
/// let error_message = "An error occurred";
|
||||
/// // Destructures ServiceResponse into request and response components
|
||||
/// let (req, res) = res.into_parts();
|
||||
/// // Create a new response with the modified body
|
||||
/// let res = res.set_body(error_message).map_into_boxed_body();
|
||||
/// // Create a new ServiceResponse with the modified response
|
||||
/// let res = dev::ServiceResponse::new(req, res).map_into_right_body();
|
||||
/// Ok(ErrorHandlerResponse::Response(res))
|
||||
///}
|
||||
/// use actix_web::{
|
||||
/// dev::ServiceResponse,
|
||||
/// http::{header, StatusCode},
|
||||
/// middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||
/// web, App, HttpResponse, Result,
|
||||
/// };
|
||||
///
|
||||
/// fn add_error_body<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// // split service response into request and response components
|
||||
/// let (req, res) = res.into_parts();
|
||||
///
|
||||
/// // set body of response to modified body
|
||||
/// let res = res.set_body("An error occurred.");
|
||||
///
|
||||
/// // modified bodies need to be boxed and placed in the "right" slot
|
||||
/// let res = ServiceResponse::new(req, res)
|
||||
/// .map_into_boxed_body()
|
||||
/// .map_into_right_body();
|
||||
///
|
||||
/// Ok(ErrorHandlerResponse::Response(res))
|
||||
/// }
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_body))
|
||||
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
|
||||
/// ```
|
||||
/// ## Registering default handler
|
||||
///
|
||||
/// Registering default handler:
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web::http::{header, StatusCode};
|
||||
/// # use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
|
||||
/// # use actix_web::{dev, web, App, HttpResponse, Result};
|
||||
/// fn add_error_header<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// # use actix_web::{
|
||||
/// # dev::ServiceResponse,
|
||||
/// # http::{header, StatusCode},
|
||||
/// # middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||
/// # web, App, HttpResponse, Result,
|
||||
/// # };
|
||||
/// fn add_error_header<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// res.response_mut().headers_mut().insert(
|
||||
/// header::CONTENT_TYPE,
|
||||
/// header::HeaderValue::from_static("Error"),
|
||||
/// );
|
||||
///
|
||||
/// // body is unchanged, map to "left" slot
|
||||
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
/// }
|
||||
///
|
||||
/// fn handle_bad_request<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// fn handle_bad_request<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// res.response_mut().headers_mut().insert(
|
||||
/// header::CONTENT_TYPE,
|
||||
/// header::HeaderValue::from_static("Bad Request Error"),
|
||||
/// );
|
||||
///
|
||||
/// // body is unchanged, map to "left" slot
|
||||
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
/// }
|
||||
///
|
||||
@@ -122,20 +145,24 @@ type DefaultHandler<B> = Option<Rc<ErrorHandler<B>>>;
|
||||
/// )
|
||||
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
|
||||
/// ```
|
||||
/// Alternatively, you can set default handlers for only client or only server errors:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use actix_web::http::{header, StatusCode};
|
||||
/// # use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
|
||||
/// # use actix_web::{dev, web, App, HttpResponse, Result};
|
||||
/// # fn add_error_header<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// You can set default handlers for all client (4xx) or all server (5xx) errors:
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web::{
|
||||
/// # dev::ServiceResponse,
|
||||
/// # http::{header, StatusCode},
|
||||
/// # middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||
/// # web, App, HttpResponse, Result,
|
||||
/// # };
|
||||
/// # fn add_error_header<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// # res.response_mut().headers_mut().insert(
|
||||
/// # header::CONTENT_TYPE,
|
||||
/// # header::HeaderValue::from_static("Error"),
|
||||
/// # );
|
||||
/// # Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
/// # }
|
||||
/// # fn handle_bad_request<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// # fn handle_bad_request<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// # res.response_mut().headers_mut().insert(
|
||||
/// # header::CONTENT_TYPE,
|
||||
/// # header::HeaderValue::from_static("Bad Request Error"),
|
||||
|
@@ -311,7 +311,6 @@ impl HttpRequest {
|
||||
|
||||
/// Load request cookies.
|
||||
#[cfg(feature = "cookies")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "cookies")))]
|
||||
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
||||
use actix_http::header::COOKIE;
|
||||
|
||||
@@ -335,7 +334,6 @@ impl HttpRequest {
|
||||
|
||||
/// Return request cookie.
|
||||
#[cfg(feature = "cookies")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "cookies")))]
|
||||
pub fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
|
||||
if let Ok(cookies) = self.cookies() {
|
||||
for cookie in cookies.iter() {
|
||||
|
@@ -21,7 +21,7 @@ use crate::{
|
||||
BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
|
||||
ServiceResponse,
|
||||
},
|
||||
Error, FromRequest, HttpResponse, Responder,
|
||||
web, Error, FromRequest, HttpResponse, Responder,
|
||||
};
|
||||
|
||||
/// A collection of [`Route`]s that respond to the same path pattern.
|
||||
@@ -38,11 +38,13 @@ use crate::{
|
||||
///
|
||||
/// let app = App::new().service(
|
||||
/// web::resource("/")
|
||||
/// .route(web::get().to(|| HttpResponse::Ok())));
|
||||
/// .get(|| HttpResponse::Ok())
|
||||
/// .post(|| async { "Hello World!" })
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// If no matching route is found, [a 405 response is returned with an appropriate Allow header][RFC
|
||||
/// 9110 §15.5.6]. This default behavior can be overridden using
|
||||
/// If no matching route is found, an empty 405 response is returned which includes an
|
||||
/// [appropriate Allow header][RFC 9110 §15.5.6]. This default behavior can be overridden using
|
||||
/// [`default_service()`](Self::default_service).
|
||||
///
|
||||
/// [RFC 9110 §15.5.6]: https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.6
|
||||
@@ -58,6 +60,7 @@ pub struct Resource<T = ResourceEndpoint> {
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
/// Constructs new resource that matches a `path` pattern.
|
||||
pub fn new<T: IntoPatterns>(path: T) -> Resource {
|
||||
let fref = Rc::new(RefCell::new(None));
|
||||
|
||||
@@ -368,6 +371,45 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! route_shortcut {
|
||||
($method_fn:ident, $method_upper:literal) => {
|
||||
#[doc = concat!(" Adds a ", $method_upper, " route.")]
|
||||
///
|
||||
/// Use [`route`](Self::route) if you need to add additional guards.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web::web;
|
||||
/// web::resource("/")
|
||||
#[doc = concat!(" .", stringify!($method_fn), "(|| async { \"Hello World!\" })")]
|
||||
/// # ;
|
||||
/// ```
|
||||
pub fn $method_fn<F, Args>(self, handler: F) -> Self
|
||||
where
|
||||
F: Handler<Args>,
|
||||
Args: FromRequest + 'static,
|
||||
F::Output: Responder + 'static,
|
||||
{
|
||||
self.route(web::$method_fn().to(handler))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Concise routes for well-known HTTP methods.
|
||||
impl<T> Resource<T>
|
||||
where
|
||||
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
|
||||
{
|
||||
route_shortcut!(get, "GET");
|
||||
route_shortcut!(post, "POST");
|
||||
route_shortcut!(put, "PUT");
|
||||
route_shortcut!(patch, "PATCH");
|
||||
route_shortcut!(delete, "DELETE");
|
||||
route_shortcut!(head, "HEAD");
|
||||
route_shortcut!(trace, "TRACE");
|
||||
}
|
||||
|
||||
impl<T, B> HttpServiceFactory for Resource<T>
|
||||
where
|
||||
T: ServiceFactory<
|
||||
|
@@ -217,7 +217,6 @@ where
|
||||
///
|
||||
/// By default handshake timeout is set to 3000 milliseconds.
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
|
||||
pub fn tls_handshake_timeout(self, dur: Duration) -> Self {
|
||||
self.config
|
||||
.lock()
|
||||
@@ -339,7 +338,7 @@ where
|
||||
/// # ; Ok(()) }
|
||||
/// ```
|
||||
pub fn bind<A: net::ToSocketAddrs>(mut self, addrs: A) -> io::Result<Self> {
|
||||
let sockets = self.bind2(addrs)?;
|
||||
let sockets = bind_addrs(addrs, self.backlog)?;
|
||||
|
||||
for lst in sockets {
|
||||
self = self.listen(lst)?;
|
||||
@@ -348,33 +347,6 @@ where
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn bind2<A: net::ToSocketAddrs>(&self, addrs: A) -> io::Result<Vec<net::TcpListener>> {
|
||||
let mut err = None;
|
||||
let mut success = false;
|
||||
let mut sockets = Vec::new();
|
||||
|
||||
for addr in addrs.to_socket_addrs()? {
|
||||
match create_tcp_listener(addr, self.backlog) {
|
||||
Ok(lst) => {
|
||||
success = true;
|
||||
sockets.push(lst);
|
||||
}
|
||||
Err(e) => err = Some(e),
|
||||
}
|
||||
}
|
||||
|
||||
if success {
|
||||
Ok(sockets)
|
||||
} else if let Some(e) = err.take() {
|
||||
Err(e)
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Can not bind to address.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves socket address(es) and binds server to created listener(s) for TLS connections
|
||||
/// using Rustls.
|
||||
///
|
||||
@@ -382,13 +354,12 @@ where
|
||||
///
|
||||
/// ALPN protocols "h2" and "http/1.1" are added to any configured ones.
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn bind_rustls<A: net::ToSocketAddrs>(
|
||||
mut self,
|
||||
addrs: A,
|
||||
config: RustlsServerConfig,
|
||||
) -> io::Result<Self> {
|
||||
let sockets = self.bind2(addrs)?;
|
||||
let sockets = bind_addrs(addrs, self.backlog)?;
|
||||
for lst in sockets {
|
||||
self = self.listen_rustls_inner(lst, config.clone())?;
|
||||
}
|
||||
@@ -402,12 +373,11 @@ where
|
||||
///
|
||||
/// ALPN protocols "h2" and "http/1.1" are added to any configured ones.
|
||||
#[cfg(feature = "openssl")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn bind_openssl<A>(mut self, addrs: A, builder: SslAcceptorBuilder) -> io::Result<Self>
|
||||
where
|
||||
A: net::ToSocketAddrs,
|
||||
{
|
||||
let sockets = self.bind2(addrs)?;
|
||||
let sockets = bind_addrs(addrs, self.backlog)?;
|
||||
let acceptor = openssl_acceptor(builder)?;
|
||||
|
||||
for lst in sockets {
|
||||
@@ -469,7 +439,6 @@ where
|
||||
///
|
||||
/// ALPN protocols "h2" and "http/1.1" are added to any configured ones.
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn listen_rustls(
|
||||
self,
|
||||
lst: net::TcpListener,
|
||||
@@ -535,7 +504,6 @@ where
|
||||
///
|
||||
/// ALPN protocols "h2" and "http/1.1" are added to any configured ones.
|
||||
#[cfg(feature = "openssl")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn listen_openssl(
|
||||
self,
|
||||
lst: net::TcpListener,
|
||||
@@ -724,6 +692,38 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind TCP listeners to socket addresses resolved from `addrs` with options.
|
||||
fn bind_addrs(
|
||||
addrs: impl net::ToSocketAddrs,
|
||||
backlog: u32,
|
||||
) -> io::Result<Vec<net::TcpListener>> {
|
||||
let mut err = None;
|
||||
let mut success = false;
|
||||
let mut sockets = Vec::new();
|
||||
|
||||
for addr in addrs.to_socket_addrs()? {
|
||||
match create_tcp_listener(addr, backlog) {
|
||||
Ok(lst) => {
|
||||
success = true;
|
||||
sockets.push(lst);
|
||||
}
|
||||
Err(e) => err = Some(e),
|
||||
}
|
||||
}
|
||||
|
||||
if success {
|
||||
Ok(sockets)
|
||||
} else if let Some(err) = err.take() {
|
||||
Err(err)
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Can not bind to address.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a TCP listener from socket address and options.
|
||||
fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result<net::TcpListener> {
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
let domain = Domain::for_address(addr);
|
||||
@@ -736,7 +736,7 @@ fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result<net::T
|
||||
Ok(net::TcpListener::from(socket))
|
||||
}
|
||||
|
||||
/// Configure `SslAcceptorBuilder` with custom server flags.
|
||||
/// Configures OpenSSL acceptor `builder` with ALPN protocols.
|
||||
#[cfg(feature = "openssl")]
|
||||
fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result<SslAcceptor> {
|
||||
builder.set_alpn_select_callback(|_, protocols| {
|
||||
|
@@ -304,7 +304,7 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_either_extract_first_try() {
|
||||
let (req, mut pl) = TestRequest::default()
|
||||
.set_form(&TestForm {
|
||||
.set_form(TestForm {
|
||||
hello: "world".to_owned(),
|
||||
})
|
||||
.to_http_parts();
|
||||
@@ -320,7 +320,7 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_either_extract_fallback() {
|
||||
let (req, mut pl) = TestRequest::default()
|
||||
.set_json(&TestForm {
|
||||
.set_json(TestForm {
|
||||
hello: "world".to_owned(),
|
||||
})
|
||||
.to_http_parts();
|
||||
@@ -351,7 +351,7 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_either_extract_recursive_fallback_inner() {
|
||||
let (req, mut pl) = TestRequest::default()
|
||||
.set_json(&TestForm {
|
||||
.set_json(TestForm {
|
||||
hello: "world".to_owned(),
|
||||
})
|
||||
.to_http_parts();
|
||||
|
@@ -62,7 +62,6 @@ actix-rt = { version = "2.1", default-features = false }
|
||||
actix-tls = { version = "3", features = ["connect", "uri"] }
|
||||
actix-utils = "3"
|
||||
|
||||
ahash = "0.7"
|
||||
base64 = "0.21"
|
||||
bytes = "1"
|
||||
cfg-if = "1"
|
||||
@@ -70,7 +69,7 @@ derive_more = "0.99.5"
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
|
||||
h2 = "0.3.9"
|
||||
http = "0.2.5"
|
||||
http = "0.2.7"
|
||||
itoa = "1"
|
||||
log =" 0.4"
|
||||
mime = "0.3"
|
||||
@@ -80,7 +79,7 @@ rand = "0.8"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7"
|
||||
tokio = { version = "1.18.5", features = ["sync"] }
|
||||
tokio = { version = "1.24.2", features = ["sync"] }
|
||||
|
||||
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
||||
|
||||
@@ -106,7 +105,7 @@ futures-util = { version = "0.3.17", default-features = false }
|
||||
static_assertions = "1.1"
|
||||
rcgen = "0.9"
|
||||
rustls-pemfile = "1"
|
||||
tokio = { version = "1.18.5", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.12"
|
||||
|
||||
[[example]]
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::VecDeque,
|
||||
collections::{HashMap, VecDeque},
|
||||
future::Future,
|
||||
io,
|
||||
ops::Deref,
|
||||
@@ -17,7 +17,6 @@ use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use actix_http::Protocol;
|
||||
use actix_rt::time::{sleep, Sleep};
|
||||
use actix_service::Service;
|
||||
use ahash::AHashMap;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures_util::FutureExt as _;
|
||||
use http::uri::Authority;
|
||||
@@ -62,7 +61,7 @@ where
|
||||
{
|
||||
fn new(config: ConnectorConfig) -> Self {
|
||||
let permits = Arc::new(Semaphore::new(config.limit));
|
||||
let available = RefCell::new(AHashMap::default());
|
||||
let available = RefCell::new(HashMap::default());
|
||||
|
||||
Self(Rc::new(ConnectionPoolInnerPriv {
|
||||
config,
|
||||
@@ -124,7 +123,7 @@ where
|
||||
Io: AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
config: ConnectorConfig,
|
||||
available: RefCell<AHashMap<Key, VecDeque<PooledConnection<Io>>>>,
|
||||
available: RefCell<HashMap<Key, VecDeque<PooledConnection<Io>>>>,
|
||||
permits: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
|
@@ -110,6 +110,7 @@
|
||||
)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
pub use actix_http::body;
|
||||
|
||||
|
Reference in New Issue
Block a user