Merge branch 'main' into feat/implement-multipartform-handling-for-optional-vector

This commit is contained in:
fasilmveloor 2025-11-23 15:41:00 +05:30 committed by GitHub
commit 62270b704d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
99 changed files with 5874 additions and 621 deletions

View File

@ -1,7 +1,8 @@
disallowed-names = [ disallowed-names = [
"e", # no single letter error bindings "..",
"e", # no single letter error bindings
] ]
disallowed-methods = [ disallowed-methods = [
"std::cell::RefCell::default()", { path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
"std::rc::Rc::default()", { path = "std::rc::Rc::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
] ]

15
.cspell.yml Normal file
View File

@ -0,0 +1,15 @@
version: "0.2"
words:
- actix
- addrs
- ALPN
- bytestring
- httparse
- MSRV
- realip
- rustls
- rustup
- serde
- uring
- webpki
- zstd

2
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [robjtede] github: [robjtede, JohnTitor]

View File

@ -1,10 +1,11 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
versioning-strategy: lockfile-only

View File

@ -2,7 +2,7 @@ name: Benchmark
on: on:
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust - name: Install Rust
run: | run: |

View File

@ -2,7 +2,7 @@ name: CI (post-merge)
on: on:
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -28,11 +28,11 @@ jobs:
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install nasm - name: Install nasm
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.1 uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
@ -44,12 +44,12 @@ jobs:
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.48.13 uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -71,19 +71,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Free Disk Space - name: Free Disk Space
run: ./scripts/free-disk-space.sh run: ./scripts/free-disk-space.sh
- name: Setup mold linker - name: Setup mold linker
uses: rui314/setup-mold@v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
- name: Install just, cargo-hack - name: Install just, cargo-hack
uses: taiki-e/install-action@v2.48.13 uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
with: with:
tool: just,cargo-hack tool: just,cargo-hack

View File

@ -6,7 +6,7 @@ on:
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -18,7 +18,7 @@ concurrency:
jobs: jobs:
read_msrv: read_msrv:
name: Read MSRV name: Read MSRV
uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0 uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@8b553824444060021f2843d7b4d803f3624d15e5 # v0.1.0
build_and_test: build_and_test:
needs: read_msrv needs: read_msrv
@ -39,11 +39,11 @@ jobs:
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install nasm - name: Install nasm
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.1 uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
@ -56,15 +56,15 @@ jobs:
- name: Setup mold linker - name: Setup mold linker
if: matrix.target.os == 'ubuntu-latest' if: matrix.target.os == 'ubuntu-latest'
uses: rui314/setup-mold@v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.48.13 uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -85,14 +85,18 @@ jobs:
- name: CI cache clean - name: CI cache clean
run: cargo-ci-cache-clean run: cargo-ci-cache-clean
- name: deny check
if: matrix.version.name == 'stable' && matrix.target.os == 'ubuntu-latest'
uses: EmbarkStudios/cargo-deny-action@f2ba7abc2abebaf185c833c3961145a3c275caad # v2.0.13
io-uring: io-uring:
name: io-uring tests name: io-uring tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
@ -105,15 +109,15 @@ jobs:
name: doc tests name: doc tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
- name: Install just - name: Install just
uses: taiki-e/install-action@v2.48.13 uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
with: with:
tool: just tool: just

View File

@ -2,7 +2,7 @@ name: Coverage
on: on:
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -15,16 +15,16 @@ jobs:
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
components: llvm-tools components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest - name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@v2.48.13 uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
with: with:
tool: just,cargo-llvm-cov,cargo-nextest tool: just,cargo-llvm-cov,cargo-nextest
@ -32,7 +32,7 @@ jobs:
run: just test-coverage-codecov run: just test-coverage-codecov
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.3.1 uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with: with:
files: codecov.json files: codecov.json
fail_ci_if_error: true fail_ci_if_error: true

View File

@ -15,10 +15,10 @@ jobs:
fmt: fmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
components: rustfmt components: rustfmt
@ -33,15 +33,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
components: clippy components: clippy
- name: Check with Clippy - name: Check with Clippy
uses: giraffate/clippy-action@v1.0.1 uses: giraffate/clippy-action@13b9d32482f25d29ead141b79e7e04e7900281e0 # v1.0.1
with: with:
reporter: github-pr-check reporter: github-pr-check
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
@ -52,10 +52,10 @@ jobs:
lint-docs: lint-docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
components: rust-docs components: rust-docs
@ -69,20 +69,20 @@ jobs:
if: false # rustdoc mismatch currently if: false # rustdoc mismatch currently
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }}) - name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just - name: Install just
uses: taiki-e/install-action@v2.48.13 uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
with: with:
tool: just tool: just
- name: Install cargo-check-external-types - name: Install cargo-check-external-types
uses: taiki-e/cache-cargo-install-action@v2.1.1 uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # v2.3.1
with: with:
tool: cargo-check-external-types tool: cargo-check-external-types

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
Cargo.lock
target/ target/
guide/build/ guide/build/
/gh-pages /gh-pages

38
.taplo.toml Normal file
View File

@ -0,0 +1,38 @@
exclude = ["target/*"]
include = ["**/*.toml"]
[formatting]
column_width = 100
align_comments = false
[[rule]]
include = ["**/Cargo.toml"]
keys = ["features"]
formatting.column_width = 105
formatting.reorder_keys = false
[[rule]]
include = ["**/Cargo.toml"]
keys = [
"dependencies",
"*-dependencies",
"workspace.dependencies",
"workspace.*-dependencies",
"target.*.dependencies",
"target.*.*-dependencies",
]
formatting.column_width = 120
formatting.reorder_keys = true
[[rule]]
include = ["**/Cargo.toml"]
keys = [
"dependencies.*",
"*-dependencies.*",
"workspace.dependencies.*",
"workspace.*-dependencies.*",
"target.*.dependencies",
"target.*.*-dependencies",
]
formatting.column_width = 120
formatting.reorder_keys = false

4219
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,17 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"actix-files", "actix-files",
"actix-http-test", "actix-http-test",
"actix-http", "actix-http",
"actix-multipart", "actix-multipart",
"actix-multipart-derive", "actix-multipart-derive",
"actix-router", "actix-router",
"actix-test", "actix-test",
"actix-web-actors", "actix-web-actors",
"actix-web-codegen", "actix-web-codegen",
"actix-web", "actix-web",
"awc", "awc",
] ]
[workspace.package] [workspace.package]

View File

@ -2,6 +2,14 @@
## Unreleased ## Unreleased
## 0.6.8
- Add `Files::with_permanent_redirect()` method.
- Change default redirect status code to 307 Temporary Redirect.
## 0.6.7
- Add `{Files, NamedFile}::read_mode_threshold()` methods to allow faster synchronous reads of small files.
- Minimum supported Rust version (MSRV) is now 1.75. - Minimum supported Rust version (MSRV) is now 1.75.
## 0.6.6 ## 0.6.6

View File

@ -1,10 +1,7 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.6" version = "0.6.8"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Static file serving for Actix Web" description = "Static file serving for Actix Web"
keywords = ["actix", "http", "async", "futures"] keywords = ["actix", "http", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -14,13 +11,7 @@ license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = ["actix_http::*", "actix_service::*", "actix_web::*", "http::*", "mime::*"]
"actix_http::*",
"actix_service::*",
"actix_web::*",
"http::*",
"mime::*",
]
[features] [features]
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
@ -46,7 +37,7 @@ v_htmlescape = "0.15.5"
# experimental-io-uring # experimental-io-uring
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
tokio-uring = { version = "0.5", optional = true, features = ["bytes"] } tokio-uring = { version = "0.5", optional = true, features = ["bytes"] }
actix-server = { version = "2.4", optional = true } # ensure matching tokio-uring versions actix-server = { version = "2.4", optional = true } # ensure matching tokio-uring versions
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7" actix-rt = "2.7"

View File

@ -3,11 +3,11 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.6)](https://docs.rs/actix-files/0.6.6) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.8)](https://docs.rs/actix-files/0.6.8)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.6/status.svg)](https://deps.rs/crate/actix-files/0.6.6) [![dependency status](https://deps.rs/crate/actix-files/0.6.8/status.svg)](https://deps.rs/crate/actix-files/0.6.8)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -14,6 +14,12 @@ use pin_project_lite::pin_project;
use super::named::File; use super::named::File;
#[derive(Debug, Clone, Copy)]
pub(crate) enum ReadMode {
Sync,
Async,
}
pin_project! { pin_project! {
/// Adapter to read a `std::file::File` in chunks. /// Adapter to read a `std::file::File` in chunks.
#[doc(hidden)] #[doc(hidden)]
@ -24,6 +30,7 @@ pin_project! {
state: ChunkedReadFileState<Fut>, state: ChunkedReadFileState<Fut>,
counter: u64, counter: u64,
callback: F, callback: F,
read_mode: ReadMode,
} }
} }
@ -57,6 +64,7 @@ pub(crate) fn new_chunked_read(
size: u64, size: u64,
offset: u64, offset: u64,
file: File, file: File,
read_mode_threshold: u64,
) -> impl Stream<Item = Result<Bytes, Error>> { ) -> impl Stream<Item = Result<Bytes, Error>> {
ChunkedReadFile { ChunkedReadFile {
size, size,
@ -69,31 +77,50 @@ pub(crate) fn new_chunked_read(
}, },
counter: 0, counter: 0,
callback: chunked_read_file_callback, callback: chunked_read_file_callback,
read_mode: if size < read_mode_threshold {
ReadMode::Sync
} else {
ReadMode::Async
},
} }
} }
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
async fn chunked_read_file_callback( fn chunked_read_file_callback_sync(
mut file: File, mut file: File,
offset: u64, offset: u64,
max_bytes: usize, max_bytes: usize,
) -> Result<(File, Bytes), Error> { ) -> Result<(File, Bytes), io::Error> {
use io::{Read as _, Seek as _}; use io::{Read as _, Seek as _};
let res = actix_web::web::block(move || { let mut buf = Vec::with_capacity(max_bytes);
let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?; file.seek(io::SeekFrom::Start(offset))?;
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
if n_bytes == 0 { if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof)) Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else { } else {
Ok((file, Bytes::from(buf))) Ok((file, Bytes::from(buf)))
}
}
#[cfg(not(feature = "experimental-io-uring"))]
#[inline]
async fn chunked_read_file_callback(
file: File,
offset: u64,
max_bytes: usize,
read_mode: ReadMode,
) -> Result<(File, Bytes), Error> {
let res = match read_mode {
ReadMode::Sync => chunked_read_file_callback_sync(file, offset, max_bytes)?,
ReadMode::Async => {
actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes))
.await??
} }
}) };
.await??;
Ok(res) Ok(res)
} }
@ -171,7 +198,7 @@ where
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
impl<F, Fut> Stream for ChunkedReadFile<F, Fut> impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
where where
F: Fn(File, u64, usize) -> Fut, F: Fn(File, u64, usize, ReadMode) -> Fut,
Fut: Future<Output = Result<(File, Bytes), Error>>, Fut: Future<Output = Result<(File, Bytes), Error>>,
{ {
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
@ -193,7 +220,7 @@ where
.take() .take()
.expect("ChunkedReadFile polled after completion"); .expect("ChunkedReadFile polled after completion");
let fut = (this.callback)(file, offset, max_bytes); let fut = (this.callback)(file, offset, max_bytes, *this.read_mode);
this.state this.state
.project_replace(ChunkedReadFileState::Future { fut }); .project_replace(ChunkedReadFileState::Future { fut });

View File

@ -41,6 +41,7 @@ pub struct Files {
index: Option<String>, index: Option<String>,
show_index: bool, show_index: bool,
redirect_to_slash: bool, redirect_to_slash: bool,
with_permanent_redirect: bool,
default: Rc<RefCell<Option<Rc<HttpNewService>>>>, default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
renderer: Rc<DirectoryRenderer>, renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>, mime_override: Option<Rc<MimeOverride>>,
@ -49,6 +50,7 @@ pub struct Files {
use_guards: Option<Rc<dyn Guard>>, use_guards: Option<Rc<dyn Guard>>,
guards: Vec<Rc<dyn Guard>>, guards: Vec<Rc<dyn Guard>>,
hidden_files: bool, hidden_files: bool,
read_mode_threshold: u64,
} }
impl fmt::Debug for Files { impl fmt::Debug for Files {
@ -64,6 +66,7 @@ impl Clone for Files {
index: self.index.clone(), index: self.index.clone(),
show_index: self.show_index, show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash, redirect_to_slash: self.redirect_to_slash,
with_permanent_redirect: self.with_permanent_redirect,
default: self.default.clone(), default: self.default.clone(),
renderer: self.renderer.clone(), renderer: self.renderer.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
@ -73,6 +76,7 @@ impl Clone for Files {
use_guards: self.use_guards.clone(), use_guards: self.use_guards.clone(),
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
read_mode_threshold: self.read_mode_threshold,
} }
} }
} }
@ -111,6 +115,7 @@ impl Files {
index: None, index: None,
show_index: false, show_index: false,
redirect_to_slash: false, redirect_to_slash: false,
with_permanent_redirect: false,
default: Rc::new(RefCell::new(None)), default: Rc::new(RefCell::new(None)),
renderer: Rc::new(directory_listing), renderer: Rc::new(directory_listing),
mime_override: None, mime_override: None,
@ -119,6 +124,7 @@ impl Files {
use_guards: None, use_guards: None,
guards: Vec::new(), guards: Vec::new(),
hidden_files: false, hidden_files: false,
read_mode_threshold: 0,
} }
} }
@ -141,6 +147,14 @@ impl Files {
self self
} }
/// Redirect with permanent redirect status code (308).
///
/// By default redirect with temporary redirect status code (307).
pub fn with_permanent_redirect(mut self) -> Self {
self.with_permanent_redirect = true;
self
}
/// Set custom directory renderer. /// Set custom directory renderer.
pub fn files_listing_renderer<F>(mut self, f: F) -> Self pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where where
@ -204,6 +218,23 @@ impl Files {
self self
} }
/// Sets the size threshold that determines file read mode (sync/async).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
///
/// Tweaking this value according to your expected usage may lead to signifiant performance
/// gains (or losses in other handlers, if `size` is too high).
///
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
///
/// Default is 0, meaning all files are read asynchronously.
pub fn read_mode_threshold(mut self, size: u64) -> Self {
self.read_mode_threshold = size;
self
}
/// Specifies whether to use ETag or not. /// Specifies whether to use ETag or not.
/// ///
/// Default is true. /// Default is true.
@ -367,6 +398,8 @@ impl ServiceFactory<ServiceRequest> for Files {
file_flags: self.file_flags, file_flags: self.file_flags,
guards: self.use_guards.clone(), guards: self.use_guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
size_threshold: self.read_mode_threshold,
with_permanent_redirect: self.with_permanent_redirect,
}; };
if let Some(ref default) = *self.default.borrow() { if let Some(ref default) = *self.default.borrow() {

View File

@ -14,7 +14,7 @@
#![warn(missing_docs, missing_debug_implementations)] #![warn(missing_docs, missing_debug_implementations)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
use std::path::Path; use std::path::Path;
@ -736,7 +736,21 @@ mod tests {
.await; .await;
let req = TestRequest::with_uri("/tests").to_request(); let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
// should redirect if index present with permanent redirect
let srv = test::init_service(
App::new().service(
Files::new("/", ".")
.index_file("test.png")
.redirect_to_slash_directory()
.with_permanent_redirect(),
),
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::PERMANENT_REDIRECT);
// should redirect if files listing is enabled // should redirect if files listing is enabled
let srv = test::init_service( let srv = test::init_service(
@ -749,7 +763,7 @@ mod tests {
.await; .await;
let req = TestRequest::with_uri("/tests").to_request(); let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
// should not redirect if the path is wrong // should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request(); let req = TestRequest::with_uri("/not_existing").to_request();

View File

@ -80,6 +80,7 @@ pub struct NamedFile {
pub(crate) content_type: Mime, pub(crate) content_type: Mime,
pub(crate) content_disposition: ContentDisposition, pub(crate) content_disposition: ContentDisposition,
pub(crate) encoding: Option<ContentEncoding>, pub(crate) encoding: Option<ContentEncoding>,
pub(crate) read_mode_threshold: u64,
} }
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
@ -200,6 +201,7 @@ impl NamedFile {
encoding, encoding,
status_code: StatusCode::OK, status_code: StatusCode::OK,
flags: Flags::default(), flags: Flags::default(),
read_mode_threshold: 0,
}) })
} }
@ -353,6 +355,23 @@ impl NamedFile {
self self
} }
/// Sets the size threshold that determines file read mode (sync/async).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
///
/// Tweaking this value according to your expected usage may lead to signifiant performance
/// gains (or losses in other handlers, if `size` is too high).
///
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
///
/// Default is 0, meaning all files are read asynchronously.
pub fn read_mode_threshold(mut self, size: u64) -> Self {
self.read_mode_threshold = size;
self
}
/// Specifies whether to return `ETag` header in response. /// Specifies whether to return `ETag` header in response.
/// ///
/// Default is true. /// Default is true.
@ -440,7 +459,8 @@ impl NamedFile {
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str())); res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
} }
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); let reader =
chunked::new_chunked_read(self.md.len(), 0, self.file, self.read_mode_threshold);
return res.streaming(reader); return res.streaming(reader);
} }
@ -577,7 +597,7 @@ impl NamedFile {
.map_into_boxed_body(); .map_into_boxed_body();
} }
let reader = chunked::new_chunked_read(length, offset, self.file); let reader = chunked::new_chunked_read(length, offset, self.file, self.read_mode_threshold);
if offset != 0 || length != self.md.len() { if offset != 0 || length != self.md.len() {
res.status(StatusCode::PARTIAL_CONTENT); res.status(StatusCode::PARTIAL_CONTENT);

View File

@ -39,6 +39,8 @@ pub struct FilesServiceInner {
pub(crate) file_flags: named::Flags, pub(crate) file_flags: named::Flags,
pub(crate) guards: Option<Rc<dyn Guard>>, pub(crate) guards: Option<Rc<dyn Guard>>,
pub(crate) hidden_files: bool, pub(crate) hidden_files: bool,
pub(crate) size_threshold: u64,
pub(crate) with_permanent_redirect: bool,
} }
impl fmt::Debug for FilesServiceInner { impl fmt::Debug for FilesServiceInner {
@ -70,7 +72,9 @@ impl FilesService {
named_file.flags = self.file_flags; named_file.flags = self.file_flags;
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); let res = named_file
.read_mode_threshold(self.size_threshold)
.into_response(&req);
ServiceResponse::new(req, res) ServiceResponse::new(req, res)
} }
@ -145,11 +149,15 @@ impl Service<ServiceRequest> for FilesService {
{ {
let redirect_to = format!("{}/", req.path()); let redirect_to = format!("{}/", req.path());
return Ok(req.into_response( let response = if this.with_permanent_redirect {
HttpResponse::Found() HttpResponse::PermanentRedirect()
.insert_header((header::LOCATION, redirect_to)) } else {
.finish(), HttpResponse::TemporaryRedirect()
)); }
.insert_header((header::LOCATION, redirect_to))
.finish();
return Ok(req.into_response(response));
} }
match this.index { match this.index {
@ -169,17 +177,7 @@ impl Service<ServiceRequest> for FilesService {
} }
} else { } else {
match NamedFile::open_async(&path).await { match NamedFile::open_async(&path).await {
Ok(mut named_file) => { Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
if let Some(ref mime_override) = this.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = this.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
Ok(ServiceResponse::new(req, res))
}
Err(err) => this.handle_err(err, req).await, Err(err) => this.handle_err(err, req).await,
} }
} }

View File

@ -7,10 +7,10 @@ keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web"
categories = [ categories = [
"network-programming", "network-programming",
"asynchronous", "asynchronous",
"web-programming::http-server", "web-programming::http-server",
"web-programming::websocket", "web-programming::websocket",
] ]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
@ -20,14 +20,14 @@ features = []
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = [
"actix_codec::*", "actix_codec::*",
"actix_http::*", "actix_http::*",
"actix_server::*", "actix_server::*",
"awc::*", "awc::*",
"bytes::*", "bytes::*",
"futures_core::*", "futures_core::*",
"http::*", "http::*",
"tokio::*", "tokio::*",
] ]
[features] [features]
@ -37,25 +37,25 @@ default = []
openssl = ["tls-openssl", "awc/openssl"] openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2"
actix-codec = "0.5" actix-codec = "0.5"
actix-tls = "3"
actix-utils = "3"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2" actix-server = "2"
actix-service = "2"
actix-tls = "3"
actix-utils = "3"
awc = { version = "3", default-features = false } awc = { version = "3", default-features = false }
bytes = "1" bytes = "1"
futures-core = { version = "0.3.17", default-features = false } futures-core = { version = "0.3.17", default-features = false }
http = "0.2.7" http = "0.2.7"
log = "0.4" log = "0.4"
socket2 = "0.5"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
slab = "0.4"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
slab = "0.4"
socket2 = "0.6"
tls-openssl = { version = "0.10.55", package = "openssl", optional = true } tls-openssl = { version = "0.10.55", package = "openssl", optional = true }
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-http = "3" actix-http = "3"

View File

@ -2,7 +2,7 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;

View File

@ -2,10 +2,31 @@
## Unreleased ## Unreleased
## 3.11.2
- Properly wake Payload receivers when feeding errors or EOF.
- Add `ServiceConfigBuilder` type to facilitate future configuration extensions.
- Add a configuration option to allow/disallow half closed connections in HTTP/1. This defaults to allow, reverting the change made in 3.11.1.
- Shutdown connections when HTTP Responses are written without reading full Requests.
## 3.11.1
- Prevent more hangs after client disconnects.
- More malformed WebSocket frames are now gracefully rejected.
- Using `TestRequest::set_payload()` now sets a Content-Length header.
## 3.11.0
- Update `brotli` dependency to `8`.
## 3.10.0
### Added ### Added
- Add `header::CLEAR_SITE_DATA` constant. - Add `header::CLEAR_SITE_DATA` constant.
- Add `Extensions::get_or_insert[_with]()` methods. - Add `Extensions::get_or_insert[_with]()` methods.
- Implement `From<Bytes>` for `Payload`.
- Implement `From<Vec<u8>>` for `Payload`.
### Changed ### Changed

View File

@ -1,58 +1,54 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.9.0" version = "3.11.2"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "HTTP types and services for the Actix ecosystem" description = "HTTP types and services for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web"
categories = [ categories = [
"network-programming", "network-programming",
"asynchronous", "asynchronous",
"web-programming::http-server", "web-programming::http-server",
"web-programming::websocket", "web-programming::websocket",
] ]
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [ features = [
"http2", "http2",
"ws", "ws",
"openssl", "openssl",
"rustls-0_20", "rustls-0_20",
"rustls-0_21", "rustls-0_21",
"rustls-0_22", "rustls-0_22",
"rustls-0_23", "rustls-0_23",
"compress-brotli", "compress-brotli",
"compress-gzip", "compress-gzip",
"compress-zstd", "compress-zstd",
] ]
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = [
"actix_codec::*", "actix_codec::*",
"actix_service::*", "actix_service::*",
"actix_tls::*", "actix_tls::*",
"actix_utils::*", "actix_utils::*",
"bytes::*", "bytes::*",
"bytestring::*", "bytestring::*",
"encoding_rs::*", "encoding_rs::*",
"futures_core::*", "futures_core::*",
"h2::*", "h2::*",
"http::*", "http::*",
"httparse::*", "httparse::*",
"language_tags::*", "language_tags::*",
"mime::*", "mime::*",
"openssl::*", "openssl::*",
"rustls::*", "rustls::*",
"tokio_util::*", "tokio_util::*",
"tokio::*", "tokio::*",
] ]
[features] [features]
@ -62,12 +58,7 @@ default = []
http2 = ["dep:h2"] http2 = ["dep:h2"]
# WebSocket protocol implementation # WebSocket protocol implementation
ws = [ ws = ["dep:local-channel", "dep:base64", "dep:rand", "dep:sha1"]
"dep:local-channel",
"dep:base64",
"dep:rand",
"dep:sha1",
]
# TLS via OpenSSL # TLS via OpenSSL
openssl = ["__tls", "actix-tls/accept", "actix-tls/openssl"] openssl = ["__tls", "actix-tls/accept", "actix-tls/openssl"]
@ -89,8 +80,8 @@ rustls-0_23 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_23"]
# Compression codecs # Compression codecs
compress-brotli = ["__compress", "dep:brotli"] compress-brotli = ["__compress", "dep:brotli"]
compress-gzip = ["__compress", "dep:flate2"] compress-gzip = ["__compress", "dep:flate2"]
compress-zstd = ["__compress", "dep:zstd"] compress-zstd = ["__compress", "dep:zstd"]
# Internal (PRIVATE!) features used to aid testing and checking feature status. # Internal (PRIVATE!) features used to aid testing and checking feature status.
# Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime. # Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime.
@ -101,10 +92,10 @@ __compress = []
__tls = [] __tls = []
[dependencies] [dependencies]
actix-service = "2"
actix-codec = "0.5" actix-codec = "0.5"
actix-utils = "3"
actix-rt = { version = "2.2", default-features = false } actix-rt = { version = "2.2", default-features = false }
actix-service = "2"
actix-utils = "3"
bitflags = "2" bitflags = "2"
bytes = "1" bytes = "1"
@ -122,16 +113,16 @@ mime = "0.3.4"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2" pin-project-lite = "0.2"
smallvec = "1.6.1" smallvec = "1.6.1"
tokio = { version = "1.24.2", features = [] } tokio = { version = "1.38.2", features = [] }
tokio-util = { version = "0.7", features = ["io", "codec"] } tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# http2 # http2
h2 = { version = "0.3.26", optional = true } h2 = { version = "0.3.27", optional = true }
# websockets # websockets
local-channel = { version = "0.1", optional = true }
base64 = { version = "0.22", optional = true } base64 = { version = "0.22", optional = true }
local-channel = { version = "0.1", optional = true }
rand = { version = "0.9", optional = true } rand = { version = "0.9", optional = true }
sha1 = { version = "0.10", optional = true } sha1 = { version = "0.10", optional = true }
@ -139,7 +130,7 @@ sha1 = { version = "0.10", optional = true }
actix-tls = { version = "3.4", default-features = false, optional = true } actix-tls = { version = "3.4", default-features = false, optional = true }
# compress-* # compress-*
brotli = { version = "7", optional = true } brotli = { version = "8", optional = true }
flate2 = { version = "1.0.13", optional = true } flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.13", optional = true } zstd = { version = "0.13", optional = true }
@ -155,17 +146,17 @@ divan = "0.1.8"
env_logger = "0.11" env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
memchr = "2.4" memchr = "2.4"
once_cell = "1.9" once_cell = "1.21"
rcgen = "0.13" rcgen = "0.13"
regex = "1.3" regex = "1.3"
rustversion = "1"
rustls-pemfile = "2" rustls-pemfile = "2"
rustversion = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.55" } tls-openssl = { package = "openssl", version = "0.10.55" }
tls-rustls_023 = { package = "rustls", version = "0.23" } tls-rustls_023 = { package = "rustls", version = "0.23" }
tokio = { version = "1.24.2", features = ["net", "rt", "macros"] } tokio = { version = "1.38.2", features = ["net", "rt", "macros", "sync"] }
[lints] [lints]
workspace = true workspace = true

View File

@ -5,11 +5,11 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.9.0)](https://docs.rs/actix-http/3.9.0) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.11.2)](https://docs.rs/actix-http/3.11.2)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.9.0/status.svg)](https://deps.rs/crate/actix-http/3.9.0) [![dependency status](https://deps.rs/crate/actix-http/3.11.2/status.svg)](https://deps.rs/crate/actix-http/3.11.2)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -31,7 +31,7 @@ async fn main() -> io::Result<()> {
actix_rt::time::sleep(Duration::from_secs(1)).await; actix_rt::time::sleep(Duration::from_secs(1)).await;
yield Err(io::Error::new(io::ErrorKind::Other, "abc")); yield Err(io::Error::other("abc"));
}))) })))
}) })
.tcp() .tcp()

View File

@ -190,7 +190,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn to_body_limit_error() { async fn to_body_limit_error() {
let err_stream = stream::once(async { Err(io::Error::new(io::ErrorKind::Other, "")) }); let err_stream = stream::once(async { Err(io::Error::other("")) });
let body = SizedStream::new(8, err_stream); let body = SizedStream::new(8, err_stream);
// not too big, but propagates error from body stream // not too big, but propagates error from body stream
assert!(to_bytes_limited(body, 10).await.unwrap().is_err()); assert!(to_bytes_limited(body, 10).await.unwrap().is_err());

View File

@ -7,7 +7,7 @@ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
h1::{self, ExpectHandler, H1Service, UpgradeHandler}, h1::{self, ExpectHandler, H1Service, UpgradeHandler},
service::HttpService, service::HttpService,
ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfig, ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfigBuilder,
}; };
/// An HTTP service builder. /// An HTTP service builder.
@ -19,6 +19,7 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
client_disconnect_timeout: Duration, client_disconnect_timeout: Duration,
secure: bool, secure: bool,
local_addr: Option<net::SocketAddr>, local_addr: Option<net::SocketAddr>,
h1_allow_half_closed: bool,
expect: X, expect: X,
upgrade: Option<U>, upgrade: Option<U>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>, on_connect_ext: Option<Rc<ConnectCallback<T>>>,
@ -40,6 +41,7 @@ where
client_disconnect_timeout: Duration::ZERO, client_disconnect_timeout: Duration::ZERO,
secure: false, secure: false,
local_addr: None, local_addr: None,
h1_allow_half_closed: true,
// dispatcher parts // dispatcher parts
expect: ExpectHandler, expect: ExpectHandler,
@ -124,6 +126,18 @@ where
self.client_disconnect_timeout(dur) self.client_disconnect_timeout(dur)
} }
/// Sets whether HTTP/1 connections should support half-closures.
///
/// Clients can choose to shutdown their writer-side of the connection after completing their
/// request and while waiting for the server response. Setting this to `false` will cause the
/// server to abort the connection handling as soon as it detects an EOF from the client.
///
/// The default behavior is to allow, i.e. `true`
pub fn h1_allow_half_closed(mut self, allow: bool) -> Self {
self.h1_allow_half_closed = allow;
self
}
/// Provide service for `EXPECT: 100-Continue` support. /// Provide service for `EXPECT: 100-Continue` support.
/// ///
/// Service get called with request that contains `EXPECT` header. /// Service get called with request that contains `EXPECT` header.
@ -142,6 +156,7 @@ where
client_disconnect_timeout: self.client_disconnect_timeout, client_disconnect_timeout: self.client_disconnect_timeout,
secure: self.secure, secure: self.secure,
local_addr: self.local_addr, local_addr: self.local_addr,
h1_allow_half_closed: self.h1_allow_half_closed,
expect: expect.into_factory(), expect: expect.into_factory(),
upgrade: self.upgrade, upgrade: self.upgrade,
on_connect_ext: self.on_connect_ext, on_connect_ext: self.on_connect_ext,
@ -166,6 +181,7 @@ where
client_disconnect_timeout: self.client_disconnect_timeout, client_disconnect_timeout: self.client_disconnect_timeout,
secure: self.secure, secure: self.secure,
local_addr: self.local_addr, local_addr: self.local_addr,
h1_allow_half_closed: self.h1_allow_half_closed,
expect: self.expect, expect: self.expect,
upgrade: Some(upgrade.into_factory()), upgrade: Some(upgrade.into_factory()),
on_connect_ext: self.on_connect_ext, on_connect_ext: self.on_connect_ext,
@ -195,13 +211,14 @@ where
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfigBuilder::new()
self.keep_alive, .keep_alive(self.keep_alive)
self.client_request_timeout, .client_request_timeout(self.client_request_timeout)
self.client_disconnect_timeout, .client_disconnect_timeout(self.client_disconnect_timeout)
self.secure, .secure(self.secure)
self.local_addr, .local_addr(self.local_addr)
); .h1_allow_half_closed(self.h1_allow_half_closed)
.build();
H1Service::with_config(cfg, service.into_factory()) H1Service::with_config(cfg, service.into_factory())
.expect(self.expect) .expect(self.expect)
@ -220,13 +237,14 @@ where
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfigBuilder::new()
self.keep_alive, .keep_alive(self.keep_alive)
self.client_request_timeout, .client_request_timeout(self.client_request_timeout)
self.client_disconnect_timeout, .client_disconnect_timeout(self.client_disconnect_timeout)
self.secure, .secure(self.secure)
self.local_addr, .local_addr(self.local_addr)
); .h1_allow_half_closed(self.h1_allow_half_closed)
.build();
crate::h2::H2Service::with_config(cfg, service.into_factory()) crate::h2::H2Service::with_config(cfg, service.into_factory())
.on_connect_ext(self.on_connect_ext) .on_connect_ext(self.on_connect_ext)
@ -242,13 +260,14 @@ where
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfigBuilder::new()
self.keep_alive, .keep_alive(self.keep_alive)
self.client_request_timeout, .client_request_timeout(self.client_request_timeout)
self.client_disconnect_timeout, .client_disconnect_timeout(self.client_disconnect_timeout)
self.secure, .secure(self.secure)
self.local_addr, .local_addr(self.local_addr)
); .h1_allow_half_closed(self.h1_allow_half_closed)
.build();
HttpService::with_config(cfg, service.into_factory()) HttpService::with_config(cfg, service.into_factory())
.expect(self.expect) .expect(self.expect)

View File

@ -1,5 +1,5 @@
use std::{ use std::{
net, net::SocketAddr,
rc::Rc, rc::Rc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -8,8 +8,76 @@ use bytes::BytesMut;
use crate::{date::DateService, KeepAlive}; use crate::{date::DateService, KeepAlive};
/// A builder for creating a [`ServiceConfig`]
#[derive(Default, Debug)]
pub struct ServiceConfigBuilder {
inner: Inner,
}
impl ServiceConfigBuilder {
/// Creates a new, default, [`ServiceConfigBuilder`]
///
/// It uses the following default values:
///
/// - [`KeepAlive::default`] for the connection keep-alive setting
/// - 5 seconds for the client request timeout
/// - 0 seconds for the client shutdown timeout
/// - secure value of `false`
/// - [`None`] for the local address setting
/// - Allow for half closed HTTP/1 connections
pub fn new() -> Self {
Self::default()
}
/// Sets the `secure` attribute for this configuration
pub fn secure(mut self, secure: bool) -> Self {
self.inner.secure = secure;
self
}
/// Sets the local address for this configuration
pub fn local_addr(mut self, local_addr: Option<SocketAddr>) -> Self {
self.inner.local_addr = local_addr;
self
}
/// Sets connection keep-alive setting
pub fn keep_alive(mut self, keep_alive: KeepAlive) -> Self {
self.inner.keep_alive = keep_alive;
self
}
/// Sets the timeout for the client to finish sending the head of its first request
pub fn client_request_timeout(mut self, timeout: Duration) -> Self {
self.inner.client_request_timeout = timeout;
self
}
/// Sets the timeout for cleanly disconnecting from the client after connection shutdown has
/// started
pub fn client_disconnect_timeout(mut self, timeout: Duration) -> Self {
self.inner.client_disconnect_timeout = timeout;
self
}
/// Sets whether HTTP/1 connections should support half-closures.
///
/// Clients can choose to shutdown their writer-side of the connection after completing their
/// request and while waiting for the server response. Setting this to `false` will cause the
/// server to abort the connection handling as soon as it detects an EOF from the client
pub fn h1_allow_half_closed(mut self, allow: bool) -> Self {
self.inner.h1_allow_half_closed = allow;
self
}
/// Builds a [`ServiceConfig`] from this [`ServiceConfigBuilder`] instance
pub fn build(self) -> ServiceConfig {
ServiceConfig(Rc::new(self.inner))
}
}
/// HTTP service configuration. /// HTTP service configuration.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct ServiceConfig(Rc<Inner>); pub struct ServiceConfig(Rc<Inner>);
#[derive(Debug)] #[derive(Debug)]
@ -18,19 +86,22 @@ struct Inner {
client_request_timeout: Duration, client_request_timeout: Duration,
client_disconnect_timeout: Duration, client_disconnect_timeout: Duration,
secure: bool, secure: bool,
local_addr: Option<std::net::SocketAddr>, local_addr: Option<SocketAddr>,
date_service: DateService, date_service: DateService,
h1_allow_half_closed: bool,
} }
impl Default for ServiceConfig { impl Default for Inner {
fn default() -> Self { fn default() -> Self {
Self::new( Self {
KeepAlive::default(), keep_alive: KeepAlive::default(),
Duration::from_secs(5), client_request_timeout: Duration::from_secs(5),
Duration::ZERO, client_disconnect_timeout: Duration::ZERO,
false, secure: false,
None, local_addr: None,
) date_service: DateService::new(),
h1_allow_half_closed: true,
}
} }
} }
@ -41,7 +112,7 @@ impl ServiceConfig {
client_request_timeout: Duration, client_request_timeout: Duration,
client_disconnect_timeout: Duration, client_disconnect_timeout: Duration,
secure: bool, secure: bool,
local_addr: Option<net::SocketAddr>, local_addr: Option<SocketAddr>,
) -> ServiceConfig { ) -> ServiceConfig {
ServiceConfig(Rc::new(Inner { ServiceConfig(Rc::new(Inner {
keep_alive: keep_alive.normalize(), keep_alive: keep_alive.normalize(),
@ -50,6 +121,7 @@ impl ServiceConfig {
secure, secure,
local_addr, local_addr,
date_service: DateService::new(), date_service: DateService::new(),
h1_allow_half_closed: true,
})) }))
} }
@ -63,7 +135,7 @@ impl ServiceConfig {
/// ///
/// Returns `None` for connections via UDS (Unix Domain Socket). /// Returns `None` for connections via UDS (Unix Domain Socket).
#[inline] #[inline]
pub fn local_addr(&self) -> Option<net::SocketAddr> { pub fn local_addr(&self) -> Option<SocketAddr> {
self.0.local_addr self.0.local_addr
} }
@ -100,6 +172,15 @@ impl ServiceConfig {
(timeout != Duration::ZERO).then(|| self.now() + timeout) (timeout != Duration::ZERO).then(|| self.now() + timeout)
} }
/// Whether HTTP/1 connections should support half-closures.
///
/// Clients can choose to shutdown their writer-side of the connection after completing their
/// request and while waiting for the server response. If this configuration is `false`, the
/// server will abort the connection handling as soon as it detects an EOF from the client
pub fn h1_allow_half_closed(&self) -> bool {
self.0.h1_allow_half_closed
}
pub(crate) fn now(&self) -> Instant { pub(crate) fn now(&self) -> Instant {
self.0.date_service.now() self.0.date_service.now()
} }

View File

@ -100,10 +100,7 @@ where
loop { loop {
if let Some(ref mut fut) = this.fut { if let Some(ref mut fut) = this.fut {
let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| { let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| {
PayloadError::Io(io::Error::new( PayloadError::Io(io::Error::other("Blocking task was cancelled unexpectedly"))
io::ErrorKind::Other,
"Blocking task was cancelled unexpectedly",
))
})??; })??;
*this.decoder = Some(decoder); *this.decoder = Some(decoder);

View File

@ -183,8 +183,7 @@ where
if let Some(ref mut fut) = this.fut { if let Some(ref mut fut) = this.fut {
let mut encoder = ready!(Pin::new(fut).poll(cx)) let mut encoder = ready!(Pin::new(fut).poll(cx))
.map_err(|_| { .map_err(|_| {
EncoderError::Io(io::Error::new( EncoderError::Io(io::Error::other(
io::ErrorKind::Other,
"Blocking task was cancelled unexpectedly", "Blocking task was cancelled unexpectedly",
)) ))
})? })?

View File

@ -415,7 +415,7 @@ mod tests {
#[test] #[test]
fn test_as_response() { fn test_as_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::other("other");
let err: Error = ParseError::Io(orig).into(); let err: Error = ParseError::Io(orig).into();
assert_eq!( assert_eq!(
format!("{}", err), format!("{}", err),
@ -425,14 +425,14 @@ mod tests {
#[test] #[test]
fn test_error_display() { fn test_error_display() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::other("other");
let err = Error::new_io().with_cause(orig); let err = Error::new_io().with_cause(orig);
assert_eq!("connection error: other", err.to_string()); assert_eq!("connection error: other", err.to_string());
} }
#[test] #[test]
fn test_error_http_response() { fn test_error_http_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::other("other");
let err = Error::new_io().with_cause(orig); let err = Error::new_io().with_cause(orig);
let resp: Response<BoxBody> = err.into(); let resp: Response<BoxBody> = err.into();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
@ -440,7 +440,7 @@ mod tests {
#[test] #[test]
fn test_payload_error() { fn test_payload_error() {
let err: PayloadError = io::Error::new(io::ErrorKind::Other, "ParseError").into(); let err: PayloadError = io::Error::other("ParseError").into();
assert!(err.to_string().contains("ParseError")); assert!(err.to_string().contains("ParseError"));
let err = PayloadError::Incomplete(None); let err = PayloadError::Incomplete(None);
@ -475,7 +475,7 @@ mod tests {
#[test] #[test]
fn test_from() { fn test_from() {
from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => ParseError::Io(..)); from_and_cause!(io::Error::other("other") => ParseError::Io(..));
from!(httparse::Error::HeaderName => ParseError::Header); from!(httparse::Error::HeaderName => ParseError::Header);
from!(httparse::Error::HeaderName => ParseError::Header); from!(httparse::Error::HeaderName => ParseError::Header);
from!(httparse::Error::HeaderValue => ParseError::Header); from!(httparse::Error::HeaderValue => ParseError::Header);

View File

@ -386,7 +386,14 @@ where
let mut this = self.project(); let mut this = self.project();
this.state.set(match size { this.state.set(match size {
BodySize::None | BodySize::Sized(0) => { BodySize::None | BodySize::Sized(0) => {
this.flags.insert(Flags::FINISHED); let payload_unfinished = this.payload.is_some();
if payload_unfinished {
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
} else {
this.flags.insert(Flags::FINISHED);
}
State::None State::None
} }
_ => State::SendPayload { body }, _ => State::SendPayload { body },
@ -404,7 +411,14 @@ where
let mut this = self.project(); let mut this = self.project();
this.state.set(match size { this.state.set(match size {
BodySize::None | BodySize::Sized(0) => { BodySize::None | BodySize::Sized(0) => {
this.flags.insert(Flags::FINISHED); let payload_unfinished = this.payload.is_some();
if payload_unfinished {
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
} else {
this.flags.insert(Flags::FINISHED);
}
State::None State::None
} }
_ => State::SendErrorPayload { body }, _ => State::SendErrorPayload { body },
@ -503,10 +517,22 @@ where
Poll::Ready(None) => { Poll::Ready(None) => {
this.codec.encode(Message::Chunk(None), this.write_buf)?; this.codec.encode(Message::Chunk(None), this.write_buf)?;
// if we have not yet pipelined to the next request, then
// this.payload was the payload for the request we just finished
// responding to. We can check to see if we finished reading it
// yet, and if not, shutdown the connection.
let payload_unfinished = this.payload.is_some();
let not_pipelined = this.messages.is_empty();
// payload stream finished. // payload stream finished.
// set state to None and handle next message // set state to None and handle next message
this.state.set(State::None); this.state.set(State::None);
this.flags.insert(Flags::FINISHED);
if not_pipelined && payload_unfinished {
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
} else {
this.flags.insert(Flags::FINISHED);
}
continue 'res; continue 'res;
} }
@ -542,10 +568,22 @@ where
Poll::Ready(None) => { Poll::Ready(None) => {
this.codec.encode(Message::Chunk(None), this.write_buf)?; this.codec.encode(Message::Chunk(None), this.write_buf)?;
// payload stream finished // if we have not yet pipelined to the next request, then
// this.payload was the payload for the request we just finished
// responding to. We can check to see if we finished reading it
// yet, and if not, shutdown the connection.
let payload_unfinished = this.payload.is_some();
let not_pipelined = this.messages.is_empty();
// payload stream finished.
// set state to None and handle next message // set state to None and handle next message
this.state.set(State::None); this.state.set(State::None);
this.flags.insert(Flags::FINISHED);
if not_pipelined && payload_unfinished {
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
} else {
this.flags.insert(Flags::FINISHED);
}
continue 'res; continue 'res;
} }
@ -1181,8 +1219,16 @@ where
let inner_p = inner.as_mut().project(); let inner_p = inner.as_mut().project();
let state_is_none = inner_p.state.is_none(); let state_is_none = inner_p.state.is_none();
// read half is closed; we do not process any responses // If the read-half is closed, we start the shutdown procedure if either is
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none { // true:
//
// - state is [`State::None`], which means that we're done with request
// processing, so if the client closed its writer-side it means that it won't
// send more requests.
// - The user requested to not allow half-closures
if inner_p.flags.contains(Flags::READ_DISCONNECT)
&& (!inner_p.config.h1_allow_half_closed() || state_is_none)
{
trace!("read half closed; start shutdown"); trace!("read half closed; start shutdown");
inner_p.flags.insert(Flags::SHUTDOWN); inner_p.flags.insert(Flags::SHUTDOWN);
} }
@ -1216,6 +1262,9 @@ where
inner_p.shutdown_timer, inner_p.shutdown_timer,
); );
if inner_p.flags.contains(Flags::SHUTDOWN) {
cx.waker().wake_by_ref();
}
Poll::Pending Poll::Pending
}; };

View File

@ -1,4 +1,10 @@
use std::{future::Future, str, task::Poll, time::Duration}; use std::{
future::Future,
pin::Pin,
str,
task::{Context, Poll},
time::Duration,
};
use actix_codec::Framed; use actix_codec::Framed;
use actix_rt::{pin, time::sleep}; use actix_rt::{pin, time::sleep};
@ -9,7 +15,7 @@ use futures_util::future::lazy;
use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags}; use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags};
use crate::{ use crate::{
body::MessageBody, body::{BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
h1::{Codec, ExpectHandler, UpgradeHandler}, h1::{Codec, ExpectHandler, UpgradeHandler},
service::HttpFlow, service::HttpFlow,
@ -17,6 +23,26 @@ use crate::{
Error, HttpMessage, KeepAlive, Method, OnConnectData, Request, Response, StatusCode, Error, HttpMessage, KeepAlive, Method, OnConnectData, Request, Response, StatusCode,
}; };
struct YieldService;
impl Service<Request> for YieldService {
type Response = Response<BoxBody>;
type Error = Response<BoxBody>;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
actix_service::always_ready!();
fn call(&self, _: Request) -> Self::Future {
Box::pin(async {
// Yield twice because the dispatcher can poll the service twice per dispatcher's poll:
// once in `handle_request` and another in `poll_response`
actix_rt::task::yield_now().await;
actix_rt::task::yield_now().await;
Ok(Response::ok())
})
}
}
fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> { fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
memchr::memmem::find(&haystack[from..], needle) memchr::memmem::find(&haystack[from..], needle)
} }
@ -509,6 +535,73 @@ async fn pipelining_ok_then_ok() {
.await; .await;
} }
#[actix_rt::test]
async fn early_response_with_payload_closes_connection() {
lazy(|cx| {
let buf = TestBuffer::new(
"\
GET /unfinished HTTP/1.1\r\n\
Content-Length: 2\r\n\
\r\n\
",
);
let cfg = ServiceConfig::new(
KeepAlive::Os,
Duration::from_millis(1),
Duration::from_millis(1),
false,
None,
);
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
buf.clone(),
services,
cfg,
None,
OnConnectData::default(),
);
pin!(h1);
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
match h1.as_mut().poll(cx) {
Poll::Pending => panic!("Should have shut down"),
Poll::Ready(res) => assert!(res.is_ok()),
}
// polls: initial => shutdown
assert_eq!(h1.poll_count, 2);
{
let mut res = buf.write_buf_slice_mut();
stabilize_date_header(&mut res);
let res = &res[..];
let exp = b"\
HTTP/1.1 200 OK\r\n\
content-length: 11\r\n\
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
/unfinished\
";
assert_eq!(
res,
exp,
"\nexpected response not in write buffer:\n\
response: {:?}\n\
expected: {:?}",
String::from_utf8_lossy(res),
String::from_utf8_lossy(exp)
);
}
})
.await;
}
#[actix_rt::test] #[actix_rt::test]
async fn pipelining_ok_then_bad() { async fn pipelining_ok_then_bad() {
lazy(|cx| { lazy(|cx| {
@ -924,6 +1017,91 @@ async fn handler_drop_payload() {
.await; .await;
} }
#[actix_rt::test]
async fn allow_half_closed() {
let buf = TestSeqBuffer::new(http_msg("GET / HTTP/1.1"));
buf.close_read();
let services = HttpFlow::new(YieldService, ExpectHandler, None::<UpgradeHandler>);
let mut cx = Context::from_waker(futures_util::task::noop_waker_ref());
let disptacher = Dispatcher::new(
buf.clone(),
services,
ServiceConfig::default(),
None,
OnConnectData::default(),
);
pin!(disptacher);
assert!(disptacher.as_mut().poll(&mut cx).is_pending());
assert_eq!(disptacher.poll_count, 1);
assert!(disptacher.as_mut().poll(&mut cx).is_ready());
assert_eq!(disptacher.poll_count, 3);
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
stabilize_date_header(&mut res);
let exp = http_msg(
r"
HTTP/1.1 200 OK
content-length: 0
date: Thu, 01 Jan 1970 12:34:56 UTC
",
);
assert_eq!(
res,
exp,
"\nexpected response not in write buffer:\n\
response: {:?}\n\
expected: {:?}",
String::from_utf8_lossy(&res),
String::from_utf8_lossy(&exp)
);
let DispatcherStateProj::Normal { inner } = disptacher.as_mut().project().inner.project()
else {
panic!("End dispatcher state should be Normal");
};
assert!(inner.state.is_none());
}
#[actix_rt::test]
async fn disallow_half_closed() {
use crate::{config::ServiceConfigBuilder, h1::dispatcher::State};
let buf = TestSeqBuffer::new(http_msg("GET / HTTP/1.1"));
buf.close_read();
let services = HttpFlow::new(YieldService, ExpectHandler, None::<UpgradeHandler>);
let config = ServiceConfigBuilder::new()
.h1_allow_half_closed(false)
.build();
let mut cx = Context::from_waker(futures_util::task::noop_waker_ref());
let disptacher = Dispatcher::new(
buf.clone(),
services,
config,
None,
OnConnectData::default(),
);
pin!(disptacher);
assert!(disptacher.as_mut().poll(&mut cx).is_pending());
assert_eq!(disptacher.poll_count, 1);
assert!(disptacher.as_mut().poll(&mut cx).is_ready());
assert_eq!(disptacher.poll_count, 2);
let res = BytesMut::from(buf.take_write_buf().as_ref());
assert!(res.is_empty());
let DispatcherStateProj::Normal { inner } = disptacher.as_mut().project().inner.project()
else {
panic!("End dispatcher state should be Normal");
};
assert!(matches!(inner.state, State::ServiceCall { .. }))
}
fn http_msg(msg: impl AsRef<str>) -> BytesMut { fn http_msg(msg: impl AsRef<str>) -> BytesMut {
let mut msg = msg let mut msg = msg
.as_ref() .as_ref()

View File

@ -310,10 +310,10 @@ impl MessageType for RequestHeadType {
Version::HTTP_11 => "HTTP/1.1", Version::HTTP_11 => "HTTP/1.1",
Version::HTTP_2 => "HTTP/2.0", Version::HTTP_2 => "HTTP/2.0",
Version::HTTP_3 => "HTTP/3.0", Version::HTTP_3 => "HTTP/3.0",
_ => return Err(io::Error::new(io::ErrorKind::Other, "unsupported version")), _ => return Err(io::Error::other("Unsupported version")),
} }
) )
.map_err(|err| io::Error::new(io::ErrorKind::Other, err)) .map_err(io::Error::other)
} }
} }
@ -433,7 +433,7 @@ impl TransferEncoding {
buf.extend_from_slice(b"0\r\n\r\n"); buf.extend_from_slice(b"0\r\n\r\n");
} else { } else {
writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len()) writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len())
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; .map_err(io::Error::other)?;
buf.reserve(msg.len() + 2); buf.reserve(msg.len() + 2);
buf.extend_from_slice(msg); buf.extend_from_slice(msg);

View File

@ -200,11 +200,13 @@ impl Inner {
#[inline] #[inline]
fn set_error(&mut self, err: PayloadError) { fn set_error(&mut self, err: PayloadError) {
self.err = Some(err); self.err = Some(err);
self.wake();
} }
#[inline] #[inline]
fn feed_eof(&mut self) { fn feed_eof(&mut self) {
self.eof = true; self.eof = true;
self.wake();
} }
#[inline] #[inline]
@ -253,8 +255,13 @@ impl Inner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{task::Poll, time::Duration};
use actix_rt::time::timeout;
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use futures_util::{FutureExt, StreamExt};
use static_assertions::{assert_impl_all, assert_not_impl_any}; use static_assertions::{assert_impl_all, assert_not_impl_any};
use tokio::sync::oneshot;
use super::*; use super::*;
@ -263,6 +270,67 @@ mod tests {
assert_impl_all!(Inner: Unpin, Send, Sync); assert_impl_all!(Inner: Unpin, Send, Sync);
const WAKE_TIMEOUT: Duration = Duration::from_secs(2);
fn prepare_waking_test(
mut payload: Payload,
expected: Option<Result<(), ()>>,
) -> (oneshot::Receiver<()>, actix_rt::task::JoinHandle<()>) {
let (tx, rx) = oneshot::channel();
let handle = actix_rt::spawn(async move {
// Make sure to poll once to set the waker
poll_fn(|cx| {
assert!(payload.poll_next_unpin(cx).is_pending());
Poll::Ready(())
})
.await;
tx.send(()).unwrap();
// actix-rt is single-threaded, so this won't race with `rx.await`
let mut pend_once = false;
poll_fn(|_| {
if pend_once {
Poll::Ready(())
} else {
// Return pending without storing wakers, we already did on the previous
// `poll_fn`, now this task will only continue if the `sender` wakes us
pend_once = true;
Poll::Pending
}
})
.await;
let got = payload.next().now_or_never().unwrap();
match expected {
Some(Ok(_)) => assert!(got.unwrap().is_ok()),
Some(Err(_)) => assert!(got.unwrap().is_err()),
None => assert!(got.is_none()),
}
});
(rx, handle)
}
#[actix_rt::test]
async fn wake_on_error() {
let (mut sender, payload) = Payload::create(false);
let (rx, handle) = prepare_waking_test(payload, Some(Err(())));
rx.await.unwrap();
sender.set_error(PayloadError::Incomplete(None));
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test]
async fn wake_on_eof() {
let (mut sender, payload) = Payload::create(false);
let (rx, handle) = prepare_waking_test(payload, None);
rx.await.unwrap();
sender.feed_eof();
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {
let (_, mut payload) = Payload::create(false); let (_, mut payload) = Payload::create(false);

View File

@ -27,7 +27,7 @@
)] )]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
pub use http::{uri, uri::Uri, Method, StatusCode, Version}; pub use http::{uri, uri::Uri, Method, StatusCode, Version};
@ -63,7 +63,7 @@ pub use self::payload::PayloadStream;
pub use self::service::TlsAcceptorConfig; pub use self::service::TlsAcceptorConfig;
pub use self::{ pub use self::{
builder::HttpServiceBuilder, builder::HttpServiceBuilder,
config::ServiceConfig, config::{ServiceConfig, ServiceConfigBuilder},
error::Error, error::Error,
extensions::Extensions, extensions::Extensions,
header::ContentEncoding, header::ContentEncoding,

View File

@ -41,13 +41,31 @@ pin_project! {
} }
impl<S> From<crate::h1::Payload> for Payload<S> { impl<S> From<crate::h1::Payload> for Payload<S> {
#[inline]
fn from(payload: crate::h1::Payload) -> Self { fn from(payload: crate::h1::Payload) -> Self {
Payload::H1 { payload } Payload::H1 { payload }
} }
} }
impl<S> From<Bytes> for Payload<S> {
#[inline]
fn from(bytes: Bytes) -> Self {
let (_, mut pl) = crate::h1::Payload::create(true);
pl.unread_data(bytes);
self::Payload::from(pl)
}
}
impl<S> From<Vec<u8>> for Payload<S> {
#[inline]
fn from(vec: Vec<u8>) -> Self {
Payload::from(Bytes::from(vec))
}
}
#[cfg(feature = "http2")] #[cfg(feature = "http2")]
impl<S> From<crate::h2::Payload> for Payload<S> { impl<S> From<crate::h2::Payload> for Payload<S> {
#[inline]
fn from(payload: crate::h2::Payload) -> Self { fn from(payload: crate::h2::Payload) -> Self {
Payload::H2 { payload } Payload::H2 { payload }
} }
@ -55,6 +73,7 @@ impl<S> From<crate::h2::Payload> for Payload<S> {
#[cfg(feature = "http2")] #[cfg(feature = "http2")]
impl<S> From<::h2::RecvStream> for Payload<S> { impl<S> From<::h2::RecvStream> for Payload<S> {
#[inline]
fn from(stream: ::h2::RecvStream) -> Self { fn from(stream: ::h2::RecvStream) -> Self {
Payload::H2 { Payload::H2 {
payload: crate::h2::Payload::new(stream), payload: crate::h2::Payload::new(stream),
@ -63,13 +82,15 @@ impl<S> From<::h2::RecvStream> for Payload<S> {
} }
impl From<BoxedPayloadStream> for Payload { impl From<BoxedPayloadStream> for Payload {
#[inline]
fn from(payload: BoxedPayloadStream) -> Self { fn from(payload: BoxedPayloadStream) -> Self {
Payload::Stream { payload } Payload::Stream { payload }
} }
} }
impl<S> Payload<S> { impl<S> Payload<S> {
/// Takes current payload and replaces it with `None` value /// Takes current payload and replaces it with `None` value.
#[must_use]
pub fn take(&mut self) -> Payload<S> { pub fn take(&mut self) -> Payload<S> {
mem::replace(self, Payload::None) mem::replace(self, Payload::None)
} }

View File

@ -11,7 +11,7 @@ use std::{
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::{Method, Uri, Version}; use http::{header, Method, Uri, Version};
use crate::{ use crate::{
header::{HeaderMap, TryIntoHeaderPair}, header::{HeaderMap, TryIntoHeaderPair},
@ -98,9 +98,13 @@ impl TestRequest {
} }
/// Set request payload. /// Set request payload.
///
/// This sets the `Content-Length` header with the size of `data`.
pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self { pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self {
let mut payload = crate::h1::Payload::empty(); let mut payload = crate::h1::Payload::empty();
payload.unread_data(data.into()); let bytes = data.into();
self.insert_header((header::CONTENT_LENGTH, bytes.len()));
payload.unread_data(bytes);
parts(&mut self.0).payload = Some(payload.into()); parts(&mut self.0).payload = Some(payload.into());
self self
} }
@ -271,6 +275,7 @@ impl TestSeqBuffer {
{ {
Self(Rc::new(RefCell::new(TestSeqInner { Self(Rc::new(RefCell::new(TestSeqInner {
read_buf: data.into(), read_buf: data.into(),
read_closed: false,
write_buf: BytesMut::new(), write_buf: BytesMut::new(),
err: None, err: None,
}))) })))
@ -289,36 +294,59 @@ impl TestSeqBuffer {
Ref::map(self.0.borrow(), |inner| &inner.write_buf) Ref::map(self.0.borrow(), |inner| &inner.write_buf)
} }
pub fn take_write_buf(&self) -> Bytes {
self.0.borrow_mut().write_buf.split().freeze()
}
pub fn err(&self) -> Ref<'_, Option<io::Error>> { pub fn err(&self) -> Ref<'_, Option<io::Error>> {
Ref::map(self.0.borrow(), |inner| &inner.err) Ref::map(self.0.borrow(), |inner| &inner.err)
} }
/// Add data to read buffer. /// Add data to read buffer.
///
/// # Panics
///
/// Panics if called after [`TestSeqBuffer::close_read`] has been called
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) { pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
self.0 let mut inner = self.0.borrow_mut();
.borrow_mut() if inner.read_closed {
.read_buf panic!("Tried to extend the read buffer after calling close_read");
.extend_from_slice(data.as_ref()) }
inner.read_buf.extend_from_slice(data.as_ref())
}
/// Closes the [`AsyncRead`]/[`Read`] part of this test buffer.
///
/// The current data in the buffer will still be returned by a call to read/poll_read, however,
/// after the buffer is empty, it will return `Ok(0)` to signify the EOF condition
pub fn close_read(&self) {
self.0.borrow_mut().read_closed = true;
} }
} }
pub struct TestSeqInner { pub struct TestSeqInner {
read_buf: BytesMut, read_buf: BytesMut,
read_closed: bool,
write_buf: BytesMut, write_buf: BytesMut,
err: Option<io::Error>, err: Option<io::Error>,
} }
impl io::Read for TestSeqBuffer { impl io::Read for TestSeqBuffer {
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> { fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
if self.0.borrow().read_buf.is_empty() { let mut inner = self.0.borrow_mut();
if self.0.borrow().err.is_some() {
Err(self.0.borrow_mut().err.take().unwrap()) if inner.read_buf.is_empty() {
if let Some(err) = inner.err.take() {
Err(err)
} else if inner.read_closed {
Ok(0)
} else { } else {
Err(io::Error::new(io::ErrorKind::WouldBlock, "")) Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
} }
} else { } else {
let size = std::cmp::min(self.0.borrow().read_buf.len(), dst.len()); let size = std::cmp::min(inner.read_buf.len(), dst.len());
let b = self.0.borrow_mut().read_buf.split_to(size); let b = inner.read_buf.split_to(size);
dst[..size].copy_from_slice(&b); dst[..size].copy_from_slice(&b);
Ok(size) Ok(size)
} }

View File

@ -94,11 +94,21 @@ impl Parser {
Some(res) => res, Some(res) => res,
}; };
let frame_len = match idx.checked_add(length) {
Some(len) => len,
None => return Err(ProtocolError::Overflow),
};
// not enough data // not enough data
if src.len() < idx + length { if src.len() < frame_len {
let min_length = min(length, max_size); let min_length = min(length, max_size);
if src.capacity() < idx + min_length { let required_cap = match idx.checked_add(min_length) {
src.reserve(idx + min_length - src.capacity()); Some(cap) => cap,
None => return Err(ProtocolError::Overflow),
};
if src.capacity() < required_cap {
src.reserve(required_cap - src.capacity());
} }
return Ok(None); return Ok(None);
} }
@ -402,4 +412,14 @@ mod tests {
Parser::write_close(&mut buf, None, false); Parser::write_close(&mut buf, None, false);
assert_eq!(&buf[..], &vec![0x88, 0x00][..]); assert_eq!(&buf[..], &vec![0x88, 0x00][..]);
} }
#[test]
fn test_parse_length_overflow() {
let buf: [u8; 14] = [
0x0a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xeb, 0x0e, 0x8f,
];
let mut buf = BytesMut::from(&buf[..]);
let result = Parser::parse(&mut buf, true, 65536);
assert!(matches!(result, Err(ProtocolError::Overflow)));
}
} }

View File

@ -11,15 +11,14 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true all-features = true
[lib] [lib]
proc-macro = true proc-macro = true
[dependencies] [dependencies]
bytesize = "2"
darling = "0.20" darling = "0.20"
parse-size = "1"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = "2" syn = "2"
@ -27,7 +26,7 @@ syn = "2"
[dev-dependencies] [dev-dependencies]
actix-multipart = "0.7" actix-multipart = "0.7"
actix-web = "4" actix-web = "4"
rustversion = "1" rustversion-msrv = "0.100"
trybuild = "1" trybuild = "1"
[lints] [lints]

View File

@ -4,13 +4,13 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::disallowed_names)] // false positives in some macro expansions #![allow(clippy::disallowed_names)] // false positives in some macro expansions
use std::collections::HashSet; use std::collections::HashSet;
use bytesize::ByteSize;
use darling::{FromDeriveInput, FromField, FromMeta}; use darling::{FromDeriveInput, FromField, FromMeta};
use parse_size::parse_size;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::Ident; use proc_macro2::Ident;
use quote::quote; use quote::quote;
@ -103,7 +103,7 @@ struct ParsedField<'t> {
/// # Field Limits /// # Field Limits
/// ///
/// You can use the `#[multipart(limit = "<size>")]` attribute to set field level limits. The limit /// You can use the `#[multipart(limit = "<size>")]` attribute to set field level limits. The limit
/// string is parsed using [parse_size]. /// string is parsed using [`bytesize`].
/// ///
/// Note: the form is also subject to the global limits configured using `MultipartFormConfig`. /// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
/// ///
@ -150,7 +150,7 @@ struct ParsedField<'t> {
/// struct Form { } /// struct Form { }
/// ``` /// ```
/// ///
/// [parse_size]: https://docs.rs/parse-size/1/parse_size /// [`bytesize`]: https://docs.rs/bytesize/2
#[proc_macro_derive(MultipartForm, attributes(multipart))] #[proc_macro_derive(MultipartForm, attributes(multipart))]
pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: syn::DeriveInput = parse_macro_input!(input); let input: syn::DeriveInput = parse_macro_input!(input);
@ -191,8 +191,8 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
let attrs = FieldAttrs::from_field(field).map_err(|err| err.write_errors())?; let attrs = FieldAttrs::from_field(field).map_err(|err| err.write_errors())?;
let serialization_name = attrs.rename.unwrap_or_else(|| rust_name.to_string()); let serialization_name = attrs.rename.unwrap_or_else(|| rust_name.to_string());
let limit = match attrs.limit.map(|limit| match parse_size(&limit) { let limit = match attrs.limit.map(|limit| match limit.parse::<ByteSize>() {
Ok(size) => Ok(usize::try_from(size).unwrap()), Ok(ByteSize(size)) => Ok(usize::try_from(size).unwrap()),
Err(err) => Err(syn::Error::new( Err(err) => Err(syn::Error::new(
field.ident.as_ref().unwrap().span(), field.ident.as_ref().unwrap().span(),
format!("Could not parse size limit `{}`: {}", limit, err), format!("Could not parse size limit `{}`: {}", limit, err),

View File

@ -1,4 +1,4 @@
#[rustversion::stable(1.72)] // MSRV #[rustversion_msrv::msrv]
#[test] #[test]
fn compile_macros() { fn compile_macros() {
let t = trybuild::TestCases::new(); let t = trybuild::TestCases::new();

View File

@ -1,16 +1,16 @@
error: Could not parse size limit `2 bytes`: invalid digit found in string error: Could not parse size limit `2 bytes`: couldn't parse "bytes" into a known SI unit, couldn't parse unit of "bytes"
--> tests/trybuild/size-limit-parse-fail.rs:6:5 --> tests/trybuild/size-limit-parse-fail.rs:6:5
| |
6 | description: Text<String>, 6 | description: Text<String>,
| ^^^^^^^^^^^ | ^^^^^^^^^^^
error: Could not parse size limit `2 megabytes`: invalid digit found in string error: Could not parse size limit `2 megabytes`: couldn't parse "megabytes" into a known SI unit, couldn't parse unit of "megabytes"
--> tests/trybuild/size-limit-parse-fail.rs:12:5 --> tests/trybuild/size-limit-parse-fail.rs:12:5
| |
12 | description: Text<String>, 12 | description: Text<String>,
| ^^^^^^^^^^^ | ^^^^^^^^^^^
error: Could not parse size limit `four meters`: invalid digit found in string error: Could not parse size limit `four meters`: couldn't parse "four meters" into a ByteSize, cannot parse float from empty string
--> tests/trybuild/size-limit-parse-fail.rs:18:5 --> tests/trybuild/size-limit-parse-fail.rs:18:5
| |
18 | description: Text<String>, 18 | description: Text<String>,

View File

@ -2,9 +2,9 @@
name = "actix-multipart" name = "actix-multipart"
version = "0.7.2" version = "0.7.2"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Jacob Halsey <jacob@jhalsey.com>", "Jacob Halsey <jacob@jhalsey.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
] ]
description = "Multipart request & form support for Actix Web" description = "Multipart request & form support for Actix Web"
keywords = ["http", "actix", "web", "multipart", "form"] keywords = ["http", "actix", "web", "multipart", "form"]
@ -14,22 +14,21 @@ license.workspace = true
edition.workspace = true edition.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true all-features = true
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = [
"actix_http::*", "actix_http::*",
"actix_multipart_derive::*", "actix_multipart_derive::*",
"actix_utils::*", "actix_utils::*",
"actix_web::*", "actix_web::*",
"bytes::*", "bytes::*",
"futures_core::*", "futures_core::*",
"mime::*", "mime::*",
"serde_json::*", "serde_json::*",
"serde_plain::*", "serde_plain::*",
"serde::*", "serde::*",
"tempfile::*", "tempfile::*",
] ]
[features] [features]
@ -55,7 +54,7 @@ serde = "1"
serde_json = "1" serde_json = "1"
serde_plain = "1" serde_plain = "1"
tempfile = { version = "3.4", optional = true } tempfile = { version = "3.4", optional = true }
tokio = { version = "1.24.2", features = ["sync", "io-util"] } tokio = { version = "1.38.2", features = ["sync", "io-util"] }
[dev-dependencies] [dev-dependencies]
actix-http = "3" actix-http = "3"
@ -66,10 +65,10 @@ actix-web = "4"
assert_matches = "1" assert_matches = "1"
awc = "3" awc = "3"
env_logger = "0.11" env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-test = "0.3" futures-test = "0.3"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
multer = "3" multer = "3"
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"
[lints] [lints]

View File

@ -24,9 +24,10 @@ Due to additional requirements for `multipart/form-data` requests, the higher le
## Examples ## Examples
```rust ```rust
use actix_web::{post, App, HttpServer, Responder}; use actix_multipart::form::{
json::Json as MpJson, tempfile::TempFile, MultipartForm, MultipartFormConfig,
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; };
use actix_web::{middleware::Logger, post, App, HttpServer, Responder};
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -36,25 +37,37 @@ struct Metadata {
#[derive(Debug, MultipartForm)] #[derive(Debug, MultipartForm)]
struct UploadForm { struct UploadForm {
// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
#[multipart(limit = "100MB")] #[multipart(limit = "100MB")]
file: TempFile, file: TempFile,
json: MpJson<Metadata>, json: MpJson<Metadata>,
} }
#[post("/videos")] #[post("/videos")]
pub async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder { async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
format!( format!(
"Uploaded file {}, with size: {}", "Uploaded file {}, with size: {}\ntemporary file ({}) was deleted\n",
form.json.name, form.file.size form.json.name,
form.file.size,
form.file.file.path().display(),
) )
} }
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
HttpServer::new(move || App::new().service(post_video)) env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
.bind(("127.0.0.1", 8080))?
.run() HttpServer::new(move || {
.await App::new()
.service(post_video)
.wrap(Logger::default())
// Also increase the global total limit to 100MiB.
.app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
})
.workers(2)
.bind(("127.0.0.1", 8080))?
.run()
.await
} }
``` ```
@ -71,4 +84,4 @@ curl -v --request POST \
<!-- cargo-rdme end --> <!-- cargo-rdme end -->
[More available in the examples repo &rarr;](https://github.com/actix/examples/tree/master/forms/multipart) [More available in the examples repo &rarr;](https://github.com/actix/examples/tree/main/forms/multipart)

View File

@ -1,4 +1,6 @@
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; use actix_multipart::form::{
json::Json as MpJson, tempfile::TempFile, MultipartForm, MultipartFormConfig,
};
use actix_web::{middleware::Logger, post, App, HttpServer, Responder}; use actix_web::{middleware::Logger, post, App, HttpServer, Responder};
use serde::Deserialize; use serde::Deserialize;
@ -9,6 +11,7 @@ struct Metadata {
#[derive(Debug, MultipartForm)] #[derive(Debug, MultipartForm)]
struct UploadForm { struct UploadForm {
// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
#[multipart(limit = "100MB")] #[multipart(limit = "100MB")]
file: TempFile, file: TempFile,
json: MpJson<Metadata>, json: MpJson<Metadata>,
@ -28,9 +31,15 @@ async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Resp
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
HttpServer::new(move || App::new().service(post_video).wrap(Logger::default())) HttpServer::new(move || {
.workers(2) App::new()
.bind(("127.0.0.1", 8080))? .service(post_video)
.run() .wrap(Logger::default())
.await // Also increase the global total limit to 100MiB.
.app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
})
.workers(2)
.bind(("127.0.0.1", 8080))?
.run()
.await
} }

View File

@ -13,7 +13,7 @@
//! ```no_run //! ```no_run
//! use actix_web::{post, App, HttpServer, Responder}; //! use actix_web::{post, App, HttpServer, Responder};
//! //!
//! use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; //! use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm, MultipartFormConfig};
//! use serde::Deserialize; //! use serde::Deserialize;
//! //!
//! #[derive(Debug, Deserialize)] //! #[derive(Debug, Deserialize)]
@ -23,6 +23,7 @@
//! //!
//! #[derive(Debug, MultipartForm)] //! #[derive(Debug, MultipartForm)]
//! struct UploadForm { //! struct UploadForm {
//! // Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
//! #[multipart(limit = "100MB")] //! #[multipart(limit = "100MB")]
//! file: TempFile, //! file: TempFile,
//! json: MpJson<Metadata>, //! json: MpJson<Metadata>,
@ -38,10 +39,15 @@
//! //!
//! #[actix_web::main] //! #[actix_web::main]
//! async fn main() -> std::io::Result<()> { //! async fn main() -> std::io::Result<()> {
//! HttpServer::new(move || App::new().service(post_video)) //! HttpServer::new(move || {
//! .bind(("127.0.0.1", 8080))? //! App::new()
//! .run() //! .service(post_video)
//! .await //! // Also increase the global total limit to 100MiB.
//! .app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
//! })
//! .bind(("127.0.0.1", 8080))?
//! .run()
//! .await
//! } //! }
//! ``` //! ```
//! //!
@ -58,7 +64,7 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
// This allows us to use the actix_multipart_derive within this crate's tests // This allows us to use the actix_multipart_derive within this crate's tests
#[cfg(test)] #[cfg(test)]

View File

@ -2,9 +2,9 @@
name = "actix-router" name = "actix-router"
version = "0.5.3" version = "0.5.3"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>", "Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
] ]
description = "Resource path matching and router" description = "Resource path matching and router"
keywords = ["actix", "router", "routing"] keywords = ["actix", "router", "routing"]
@ -13,10 +13,7 @@ license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = ["http::*", "serde::*"]
"http::*",
"serde::*",
]
[features] [features]
default = ["http", "unicode"] default = ["http", "unicode"]
@ -35,8 +32,8 @@ tracing = { version = "0.1.30", default-features = false, features = ["log"] }
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.5", features = ["html_reports"] }
http = "0.2.7" http = "0.2.7"
serde = { version = "1", features = ["derive"] }
percent-encoding = "2.1" percent-encoding = "2.1"
serde = { version = "1", features = ["derive"] }
[lints] [lints]
workspace = true workspace = true

View File

@ -13,6 +13,7 @@ macro_rules! register {
register!(finish => "(.*)", "(.*)", "(.*)", "(.*)") register!(finish => "(.*)", "(.*)", "(.*)", "(.*)")
}}; }};
(finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{
#[expect(clippy::useless_concat)]
let arr = [ let arr = [
concat!("/authorizations"), concat!("/authorizations"),
concat!("/authorizations/", $p1), concat!("/authorizations/", $p1),

View File

@ -2,7 +2,7 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
mod de; mod de;
mod path; mod path;

View File

@ -1,37 +1,34 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.5" version = "0.1.5"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Integration testing tools for Actix Web applications" description = "Integration testing tools for Actix Web applications"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web"
categories = [ categories = [
"network-programming", "network-programming",
"asynchronous", "asynchronous",
"web-programming::http-server", "web-programming::http-server",
"web-programming::websocket", "web-programming::websocket",
] ]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = [
"actix_codec::*", "actix_codec::*",
"actix_http_test::*", "actix_http_test::*",
"actix_http::*", "actix_http::*",
"actix_service::*", "actix_service::*",
"actix_web::*", "actix_web::*",
"awc::*", "awc::*",
"bytes::*", "bytes::*",
"futures_core::*", "futures_core::*",
"http::*", "http::*",
"openssl::*", "openssl::*",
"rustls::*", "rustls::*",
"tokio::*", "tokio::*",
] ]
[features] [features]
@ -72,7 +69,7 @@ tls-rustls-0_20 = { package = "rustls", version = "0.20", optional = true }
tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true } tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true }
tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true } tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true }
tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true } tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true }
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
[lints] [lints]
workspace = true workspace = true

View File

@ -29,7 +29,7 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;

View File

@ -12,32 +12,32 @@ rust-version.workspace = true
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = [
"actix::*", "actix::*",
"actix_http::*", "actix_http::*",
"actix_web::*", "actix_web::*",
"bytes::*", "bytes::*",
"bytestring::*", "bytestring::*",
"futures_core::*", "futures_core::*",
] ]
[dependencies] [dependencies]
actix = { version = ">=0.12, <0.14", default-features = false } actix = { version = ">=0.12, <0.14", default-features = false }
actix-codec = "0.5" actix-codec = "0.5"
actix-http = "3" actix-http = "3"
actix-web = { version = "4", default-features = false } actix-web = { version = "4", default-features = false, features = ["ws"] }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
futures-core = { version = "0.3.17", default-features = false } futures-core = { version = "0.3.17", default-features = false }
pin-project-lite = "0.2" pin-project-lite = "0.2"
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
tokio-util = { version = "0.7", features = ["codec"] } tokio-util = { version = "0.7", features = ["codec"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1" actix-test = "0.1"
awc = { version = "3", default-features = false }
actix-web = { version = "4", features = ["macros"] } actix-web = { version = "4", features = ["macros"] }
awc = { version = "3", default-features = false }
env_logger = "0.11" env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] } futures-util = { version = "0.3.17", default-features = false, features = ["std"] }

View File

@ -59,7 +59,7 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
mod context; mod context;
pub mod ws; pub mod ws;

View File

@ -776,10 +776,7 @@ where
} }
Poll::Pending => break, Poll::Pending => break,
Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(err))) => {
return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::new( return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::other(err)))));
io::ErrorKind::Other,
format!("{err}"),
)))));
} }
} }
} }
@ -795,11 +792,10 @@ where
} }
Some(frm) => { Some(frm) => {
let msg = match frm { let msg = match frm {
Frame::Text(data) => { Frame::Text(data) => Message::Text(
Message::Text(ByteString::try_from(data).map_err(|err| { ByteString::try_from(data)
ProtocolError::Io(io::Error::new(io::ErrorKind::Other, err)) .map_err(|err| ProtocolError::Io(io::Error::other(err)))?,
})?) ),
}
Frame::Binary(data) => Message::Binary(data), Frame::Binary(data) => Message::Binary(data),
Frame::Ping(s) => Message::Ping(s), Frame::Ping(s) => Message::Ping(s),
Frame::Pong(s) => Message::Pong(s), Frame::Pong(s) => Message::Pong(s),

View File

@ -2,10 +2,7 @@
name = "actix-web-codegen" name = "actix-web-codegen"
version = "4.3.0" version = "4.3.0"
description = "Routing and runtime macros for Actix Web" description = "Routing and runtime macros for Actix Web"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
homepage.workspace = true homepage.workspace = true
repository.workspace = true repository.workspace = true
license.workspace = true license.workspace = true
@ -33,8 +30,8 @@ actix-utils = "3"
actix-web = "4" actix-web = "4"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
rustversion-msrv = "0.100"
trybuild = "1" trybuild = "1"
rustversion = "1"
[lints] [lints]
workspace = true workspace = true

View File

@ -75,7 +75,7 @@
#![recursion_limit = "512"] #![recursion_limit = "512"]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;

View File

@ -59,6 +59,7 @@ macro_rules! standard_method_type {
( (
$($variant:ident, $upper:ident, $lower:ident,)+ $($variant:ident, $upper:ident, $lower:ident,)+
) => { ) => {
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MethodType { pub enum MethodType {
$( $(
@ -466,7 +467,7 @@ impl ToTokens for Route {
let stream = quote! { let stream = quote! {
#(#doc_attributes)* #(#doc_attributes)*
#[allow(non_camel_case_types, missing_docs)] #[allow(non_camel_case_types)]
#vis struct #name; #vis struct #name;
impl ::actix_web::dev::HttpServiceFactory for #name { impl ::actix_web::dev::HttpServiceFactory for #name {

View File

@ -1,4 +1,4 @@
#[rustversion::stable(1.72)] // MSRV #[rustversion_msrv::msrv]
#[test] #[test]
fn compile_macros() { fn compile_macros() {
let t = trybuild::TestCases::new(); let t = trybuild::TestCases::new();

View File

@ -2,7 +2,40 @@
## Unreleased ## Unreleased
## 4.12.0
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now sets `Content-Type` to `application/octet-stream` if `Content-Type` does not exist.
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now calls `actix_web::response::builder::HttpResponseBuilder::no_chunking()` and returns `SizedStream` if `Content-Length` is set by user.
- Add `ws` crate feature (on-by-default) which forwards to `actix-http` and guards some of its `ResponseError` impls.
- Add public export for `EitherExtractError` in `error` module.
## 4.11.0
- Add `Logger::log_level()` method.
- Improve handling of non-UTF-8 header values in `Logger` middleware.
- Add `HttpServer::shutdown_signal()` method.
- Mark `HttpServer` as `#[must_use]`.
- Allow SVG images to be compressed by the `Compress` middleware.
- Ignore `Host` header in `Host` guard when connection protocol is HTTP/2.
- Re-export `mime` dependency.
- Update `brotli` dependency to `8`.
## 4.10.2
- No significant changes since `4.10.1`.
## 4.10.1
- No significant changes since `4.10.0`.
## 4.10.0
### Added
- Implement `Responder` for `Result<(), E: Into<Error>>`. Returning `Ok(())` responds with HTTP 204 No Content. - Implement `Responder` for `Result<(), E: Into<Error>>`. Returning `Ok(())` responds with HTTP 204 No Content.
### Changed
- On Windows, an error is now returned from `HttpServer::bind()` (or TLS variants) when binding to a socket that's already in use. - On Windows, an error is now returned from `HttpServer::bind()` (or TLS variants) when binding to a socket that's already in use.
- Update `brotli` dependency to `7`. - Update `brotli` dependency to `7`.
- Minimum supported Rust version (MSRV) is now 1.75. - Minimum supported Rust version (MSRV) is now 1.75.

View File

@ -1,17 +1,14 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.9.0" version = "4.12.0"
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
categories = [ categories = [
"network-programming", "network-programming",
"asynchronous", "asynchronous",
"web-programming::http-server", "web-programming::http-server",
"web-programming::websocket" "web-programming::websocket",
] ]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web"
@ -20,57 +17,56 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [ features = [
"macros", "macros",
"openssl", "openssl",
"rustls-0_20", "rustls-0_20",
"rustls-0_21", "rustls-0_21",
"rustls-0_22", "rustls-0_22",
"rustls-0_23", "rustls-0_23",
"compress-brotli", "compress-brotli",
"compress-gzip", "compress-gzip",
"compress-zstd", "compress-zstd",
"cookies", "cookies",
"secure-cookies", "secure-cookies",
] ]
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = [
"actix_http::*", "actix_http::*",
"actix_router::*", "actix_router::*",
"actix_rt::*", "actix_rt::*",
"actix_server::*", "actix_server::*",
"actix_service::*", "actix_service::*",
"actix_utils::*", "actix_utils::*",
"actix_web_codegen::*", "actix_web_codegen::*",
"bytes::*", "bytes::*",
"cookie::*", "cookie::*",
"cookie", "cookie",
"futures_core::*", "futures_core::*",
"http::*", "http::*",
"language_tags::*", "language_tags::*",
"mime::*", "mime::*",
"openssl::*", "openssl::*",
"rustls::*", "rustls::*",
"serde_json::*", "serde_json::*",
"serde_urlencoded::*", "serde_urlencoded::*",
"serde::*", "serde::*",
"serde::*", "tokio::*",
"tokio::*", "url::*",
"url::*",
] ]
[features] [features]
default = [ default = [
"macros", "macros",
"compress-brotli", "compress-brotli",
"compress-gzip", "compress-gzip",
"compress-zstd", "compress-zstd",
"cookies", "cookies",
"http2", "http2",
"unicode", "unicode",
"compat", "compat",
"ws",
] ]
# Brotli algorithm content-encoding support # Brotli algorithm content-encoding support
@ -89,9 +85,12 @@ cookies = ["dep:cookie"]
# Secure & signed cookies # Secure & signed cookies
secure-cookies = ["cookies", "cookie/secure"] secure-cookies = ["cookies", "cookie/secure"]
# HTTP/2 support (including h2c). # HTTP/2 support (including h2c)
http2 = ["actix-http/http2"] http2 = ["actix-http/http2"]
# WebSocket support
ws = ["actix-http/ws"]
# TLS via OpenSSL # TLS via OpenSSL
openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
@ -121,9 +120,7 @@ __tls = []
experimental-io-uring = ["actix-server/io-uring"] experimental-io-uring = ["actix-server/io-uring"]
# Feature group which, when disabled, helps migrate code to v5.0. # Feature group which, when disabled, helps migrate code to v5.0.
compat = [ compat = ["compat-routing-macros-force-pub"]
"compat-routing-macros-force-pub",
]
# Opt-out forwards-compatibility for handler visibility inheritance fix. # Opt-out forwards-compatibility for handler visibility inheritance fix.
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"] compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]
@ -132,12 +129,12 @@ compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-for
actix-codec = "0.5" actix-codec = "0.5"
actix-macros = { version = "0.2.3", optional = true } actix-macros = { version = "0.2.3", optional = true }
actix-rt = { version = "2.6", default-features = false } actix-rt = { version = "2.6", default-features = false }
actix-server = "2" actix-server = "2.6"
actix-service = "2" actix-service = "2"
actix-utils = "3"
actix-tls = { version = "3.4", default-features = false, optional = true } actix-tls = { version = "3.4", default-features = false, optional = true }
actix-utils = "3"
actix-http = { version = "3.7", features = ["ws"] } actix-http = "3.11"
actix-router = { version = "0.5.3", default-features = false, features = ["http"] } actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
actix-web-codegen = { version = "4.3", optional = true, default-features = false } actix-web-codegen = { version = "4.3", optional = true, default-features = false }
@ -145,17 +142,17 @@ bytes = "1"
bytestring = "1" bytestring = "1"
cfg-if = "1" cfg-if = "1"
cookie = { version = "0.16", features = ["percent-encode"], optional = true } cookie = { version = "0.16", features = ["percent-encode"], optional = true }
derive_more = { version = "2", features = ["display", "error", "from"] } derive_more = { version = "2", features = ["as_ref", "deref", "deref_mut", "display", "error", "from"] }
encoding_rs = "0.8" encoding_rs = "0.8"
foldhash = "0.1" foldhash = "0.1"
futures-core = { version = "0.3.17", default-features = false } futures-core = { version = "0.3.17", default-features = false }
futures-util = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false }
itoa = "1"
impl-more = "0.1.4" impl-more = "0.1.4"
itoa = "1"
language-tags = "0.3" language-tags = "0.3"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
once_cell = "1.5" once_cell = "1.21"
pin-project-lite = "0.2.7" pin-project-lite = "0.2.7"
regex = { version = "1.5.5", optional = true } regex = { version = "1.5.5", optional = true }
regex-lite = "0.1" regex-lite = "0.1"
@ -163,18 +160,18 @@ serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
smallvec = "1.6.1" smallvec = "1.6.1"
tracing = "0.1.30" socket2 = "0.6"
socket2 = "0.5"
time = { version = "0.3", default-features = false, features = ["formatting"] } time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1" tracing = "0.1.30"
url = "2.5.4"
[dev-dependencies] [dev-dependencies]
actix-files = "0.6" actix-files = "0.6"
actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] } actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] }
awc = { version = "3", features = ["openssl"] } awc = { version = "3", features = ["openssl"] }
brotli = "7" brotli = "8"
const-str = "0.5" const-str = "0.5" # TODO(MSRV 1.77): update to 0.6
core_affinity = "0.8" core_affinity = "0.8"
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.5", features = ["html_reports"] }
env_logger = "0.11" env_logger = "0.11"
@ -187,7 +184,8 @@ serde = { version = "1", features = ["derive"] }
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.55" } tls-openssl = { package = "openssl", version = "0.10.55" }
tls-rustls = { package = "rustls", version = "0.23" } tls-rustls = { package = "rustls", version = "0.23" }
tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.38.2", features = ["rt-multi-thread", "macros"] }
tokio-util = "0.7"
zstd = "0.13" zstd = "0.13"
[lints] [lints]

View File

@ -3,7 +3,6 @@
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`. - The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
- Cookie handling has been offloaded to the `cookie` crate: - Cookie handling has been offloaded to the `cookie` crate:
- `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs. - `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
- Some types now require lifetime parameters. - Some types now require lifetime parameters.

View File

@ -115,7 +115,7 @@ An alternative [path param type with public field but no `Deref` impl is availab
## Rustls Crate Upgrade ## Rustls Crate Upgrade
Actix Web now depends on version 0.20 of `rustls`. As a result, the server config builder has changed. [See the updated example project.](https://github.com/actix/examples/tree/master/https-tls/rustls/) Actix Web now depends on version 0.20 of `rustls`. As a result, the server config builder has changed. [See the updated example project.](https://github.com/actix/examples/tree/main/https-tls/rustls/)
## Removed `awc` Client Re-export ## Removed `awc` Client Re-export

View File

@ -8,10 +8,10 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.9.0)](https://docs.rs/actix-web/4.9.0) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.12.0)](https://docs.rs/actix-web/4.12.0)
![MSRV](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MSRV](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.9.0/status.svg)](https://deps.rs/crate/actix-web/4.9.0) [![Dependency Status](https://deps.rs/crate/actix-web/4.12.0/status.svg)](https://deps.rs/crate/actix-web/4.12.0)
<br /> <br />
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/graph/badge.svg?token=dSwOnp9QCv)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/graph/badge.svg?token=dSwOnp9QCv)](https://codecov.io/gh/actix/actix-web)
@ -44,7 +44,7 @@
- [Website & User Guide](https://actix.rs) - [Website & User Guide](https://actix.rs)
- [Examples Repository](https://github.com/actix/examples) - [Examples Repository](https://github.com/actix/examples)
- [API Documentation](https://docs.rs/actix-web) - [API Documentation](https://docs.rs/actix-web)
- [API Documentation (master branch)](https://actix.rs/actix-web/actix_web) - [API Documentation (mainranch)](https://actix.rs/actix-web/actix_web)
## Example ## Example
@ -78,23 +78,23 @@ async fn main() -> std::io::Result<()> {
### More Examples ### More Examples
- [Hello World](https://github.com/actix/examples/tree/master/basics/hello-world) - [Hello World](https://github.com/actix/examples/tree/mainasics/hello-world)
- [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics) - [Basic Setup](https://github.com/actix/examples/tree/mainasics/basics)
- [Application State](https://github.com/actix/examples/tree/master/basics/state) - [Application State](https://github.com/actix/examples/tree/mainasics/state)
- [JSON Handling](https://github.com/actix/examples/tree/master/json/json) - [JSON Handling](https://github.com/actix/examples/tree/mainson/json)
- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart) - [Multipart Streams](https://github.com/actix/examples/tree/mainorms/multipart)
- [MongoDB Integration](https://github.com/actix/examples/tree/master/databases/mongodb) - [MongoDB Integration](https://github.com/actix/examples/tree/mainatabases/mongodb)
- [Diesel Integration](https://github.com/actix/examples/tree/master/databases/diesel) - [Diesel Integration](https://github.com/actix/examples/tree/mainatabases/diesel)
- [SQLite Integration](https://github.com/actix/examples/tree/master/databases/sqlite) - [SQLite Integration](https://github.com/actix/examples/tree/mainatabases/sqlite)
- [Postgres Integration](https://github.com/actix/examples/tree/master/databases/postgres) - [Postgres Integration](https://github.com/actix/examples/tree/mainatabases/postgres)
- [Tera Templates](https://github.com/actix/examples/tree/master/templating/tera) - [Tera Templates](https://github.com/actix/examples/tree/mainemplating/tera)
- [Askama Templates](https://github.com/actix/examples/tree/master/templating/askama) - [Askama Templates](https://github.com/actix/examples/tree/mainemplating/askama)
- [HTTPS using Rustls](https://github.com/actix/examples/tree/master/https-tls/rustls) - [HTTPS using Rustls](https://github.com/actix/examples/tree/mainttps-tls/rustls)
- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/https-tls/openssl) - [HTTPS using OpenSSL](https://github.com/actix/examples/tree/mainttps-tls/openssl)
- [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets) - [Simple WebSocket](https://github.com/actix/examples/tree/mainebsockets)
- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat) - [WebSocket Chat](https://github.com/actix/examples/tree/mainebsockets/chat)
You may consider checking out [this directory](https://github.com/actix/examples/tree/master) for more examples. You may consider checking out [this directory](https://github.com/actix/examples/tree/mainfor more examples.
## Benchmarks ## Benchmarks

View File

@ -2,7 +2,7 @@
//! properties and pass them to a handler through request-local data. //! properties and pass them to a handler through request-local data.
//! //!
//! For an example of extracting a client TLS certificate, see: //! For an example of extracting a client TLS certificate, see:
//! <https://github.com/actix/examples/tree/master/https-tls/rustls-client-cert> //! <https://github.com/actix/examples/tree/main/https-tls/rustls-client-cert>
use std::{any::Any, io, net::SocketAddr}; use std::{any::Any, io, net::SocketAddr};

View File

@ -21,6 +21,7 @@ mod response_error;
pub(crate) use self::macros::{downcast_dyn, downcast_get_type_id}; pub(crate) use self::macros::{downcast_dyn, downcast_get_type_id};
pub use self::{error::Error, internal::*, response_error::ResponseError}; pub use self::{error::Error, internal::*, response_error::ResponseError};
pub use crate::types::EitherExtractError;
/// A convenience [`Result`](std::result::Result) for Actix Web operations. /// A convenience [`Result`](std::result::Result) for Actix Web operations.
/// ///

View File

@ -7,7 +7,6 @@ use std::{
io::{self, Write as _}, io::{self, Write as _},
}; };
use actix_http::Response;
use bytes::BytesMut; use bytes::BytesMut;
use crate::{ use crate::{
@ -126,20 +125,24 @@ impl ResponseError for actix_http::error::PayloadError {
} }
} }
impl ResponseError for actix_http::ws::ProtocolError {}
impl ResponseError for actix_http::error::ContentTypeError { impl ResponseError for actix_http::error::ContentTypeError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
} }
} }
#[cfg(feature = "ws")]
impl ResponseError for actix_http::ws::HandshakeError { impl ResponseError for actix_http::ws::HandshakeError {
fn error_response(&self) -> HttpResponse<BoxBody> { fn error_response(&self) -> HttpResponse<BoxBody> {
Response::from(self).map_into_boxed_body().into() actix_http::Response::from(self)
.map_into_boxed_body()
.into()
} }
} }
#[cfg(feature = "ws")]
impl ResponseError for actix_http::ws::ProtocolError {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,4 +1,4 @@
use actix_http::{header, uri::Uri, RequestHead}; use actix_http::{header, uri::Uri, RequestHead, Version};
use super::{Guard, GuardContext}; use super::{Guard, GuardContext};
@ -66,6 +66,7 @@ fn get_host_uri(req: &RequestHead) -> Option<Uri> {
req.headers req.headers
.get(header::HOST) .get(header::HOST)
.and_then(|host_value| host_value.to_str().ok()) .and_then(|host_value| host_value.to_str().ok())
.filter(|_| req.version < Version::HTTP_2)
.or_else(|| req.uri.host()) .or_else(|| req.uri.host())
.and_then(|host| host.parse().ok()) .and_then(|host| host.parse().ok())
} }
@ -123,6 +124,38 @@ mod tests {
use super::*; use super::*;
use crate::test::TestRequest; use crate::test::TestRequest;
#[test]
fn host_not_from_header_if_http2() {
let req = TestRequest::default()
.uri("www.rust-lang.org")
.insert_header((
header::HOST,
header::HeaderValue::from_static("www.example.com"),
))
.to_srv_request();
let host = Host("www.example.com");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let req = TestRequest::default()
.version(actix_http::Version::HTTP_2)
.uri("www.rust-lang.org")
.insert_header((
header::HOST,
header::HeaderValue::from_static("www.example.com"),
))
.to_srv_request();
let host = Host("www.example.com");
assert!(!host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
}
#[test] #[test]
fn host_from_header() { fn host_from_header() {
let req = TestRequest::default() let req = TestRequest::default()

View File

@ -70,7 +70,7 @@ use crate::{
/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the /// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the
/// bounds of the handler call after argument extraction: /// bounds of the handler call after argument extraction:
/// ```ignore /// ```ignore
/// impl<Func, Arg1, Arg2, Fut> Handler<(Arg1, Arg2)> for Func /// impl<Func, Fut, Arg1, Arg2> Handler<(Arg1, Arg2)> for Func
/// where /// where
/// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static, /// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static,
/// Fut: Future, /// Fut: Future,

View File

@ -267,7 +267,7 @@ impl DispositionParam {
/// parameters: vec![DispositionParam::FilenameExt(ExtendedValue { /// parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
/// charset: Charset::Iso_8859_1, // The character set for the bytes of the filename /// charset: Charset::Iso_8859_1, // The character set for the bytes of the filename
/// language_tag: None, // The optional language tag (see `language-tag` crate) /// language_tag: None, // The optional language tag (see `language-tag` crate)
/// value: b"\xa9 Copyright 1989.txt".to_vec(), // the actual bytes of the filename /// value: b"\xA9 Ferris 2011.txt".to_vec(), // the actual bytes of the filename
/// })], /// })],
/// }; /// };
/// assert!(cd1.is_attachment()); /// assert!(cd1.is_attachment());

View File

@ -158,7 +158,7 @@ impl ConnectionInfo {
/// The address is resolved through the following, in order: /// The address is resolved through the following, in order:
/// - `Forwarded` header /// - `Forwarded` header
/// - `X-Forwarded-For` header /// - `X-Forwarded-For` header
/// - peer address of opened socket (same as [`remote_addr`](Self::remote_addr)) /// - peer address of opened socket (same as [`peer_addr`](Self::peer_addr))
/// ///
/// # Security /// # Security
/// Do not use this function for security purposes unless you can be sure that the `Forwarded` /// Do not use this function for security purposes unless you can be sure that the `Forwarded`

View File

@ -72,13 +72,13 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
pub use actix_http::{body, HttpMessage}; pub use actix_http::{body, HttpMessage};
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
#[doc(inline)] #[doc(inline)]
pub use cookie; pub use cookie;
pub use mime;
mod app; mod app;
mod app_service; mod app_service;
mod config; mod config;

View File

@ -2,16 +2,79 @@
## What Is A Middleware? ## What Is A Middleware?
Middleware in Actix Web is a powerful mechanism that allows you to add additional behavior to request/response processing. It enables you to:
- Pre-process incoming requests (e.g., path normalization, authentication)
- Post-process outgoing responses (e.g., logging, compression)
- Modify application state through ServiceRequest
- Access external services (e.g., sessions, caching)
Middleware is registered for each App, Scope, or Resource and executed in the reverse order of registration. This means the last registered middleware is the first to process the request.
## Middleware Traits ## Middleware Traits
Actix Web's middleware system is built on two main traits:
1. `Transform<S, Req>`: The builder trait that creates the actual Service. It's responsible for:
- Creating new middleware instances
- Assembling the middleware chain
- Handling initialization errors
2. `Service<Req>`: The trait that represents the actual middleware functionality. It:
- Processes requests and responses
- Can modify both request and response
- Can short-circuit request processing
- Must be implemented for the middleware to work
## Understanding Body Types ## Understanding Body Types
When working with middleware, it's important to understand body types:
- Middleware can work with different body types for requests and responses
- The `MessageBody` trait is used to handle different body types
- You can use `EitherBody` when you need to handle multiple body types
- Be careful with body consumption - once a body is consumed, it cannot be read again
## Best Practices ## Best Practices
1. Keep middleware focused and single-purpose
2. Handle errors appropriately and propagate them correctly
3. Be mindful of performance impact
4. Use appropriate body types and handle them correctly
5. Consider middleware ordering carefully
6. Document your middleware's behavior and requirements
7. Test your middleware thoroughly
## Error Propagation ## Error Propagation
Proper error handling is crucial in middleware:
1. Always propagate errors from the inner service
2. Use appropriate error types
3. Handle initialization errors
4. Consider using custom error types for specific middleware errors
5. Document error conditions and handling
## When To (Not) Use Middleware ## When To (Not) Use Middleware
Use middleware when you need to:
- Add cross-cutting concerns
- Modify requests/responses globally
- Add authentication/authorization
- Add logging or monitoring
- Handle compression or caching
Avoid middleware when:
- The functionality is specific to a single route
- The operation is better handled by a service
- The overhead would be too high
- The functionality can be implemented more simply
## Author's References ## Author's References
- `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428 - `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428
- Actix Web Documentation: https://docs.rs/actix-web
- Service Trait Documentation: https://docs.rs/actix-service
- MessageBody Trait Documentation: https://docs.rs/actix-web/latest/actix_web/body/trait.MessageBody.html

View File

@ -191,8 +191,10 @@ where
None => true, None => true,
Some(hdr) => { Some(hdr) => {
match hdr.to_str().ok().and_then(|hdr| hdr.parse::<Mime>().ok()) { 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_() == mime::IMAGE => {
Some(mime) if mime.type_().as_str() == "video" => false, matches!(mime.subtype(), mime::SVG)
}
Some(mime) if mime.type_() == mime::VIDEO => false,
_ => true, _ => true,
} }
} }

View File

@ -16,7 +16,7 @@ use actix_service::{Service, Transform};
use actix_utils::future::{ready, Ready}; use actix_utils::future::{ready, Ready};
use bytes::Bytes; use bytes::Bytes;
use futures_core::ready; use futures_core::ready;
use log::{debug, warn}; use log::{debug, warn, Level};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
#[cfg(feature = "unicode")] #[cfg(feature = "unicode")]
use regex::Regex; use regex::Regex;
@ -92,6 +92,7 @@ struct Inner {
exclude: HashSet<String>, exclude: HashSet<String>,
exclude_regex: Vec<Regex>, exclude_regex: Vec<Regex>,
log_target: Cow<'static, str>, log_target: Cow<'static, str>,
log_level: Level,
} }
impl Logger { impl Logger {
@ -102,6 +103,7 @@ impl Logger {
exclude: HashSet::new(), exclude: HashSet::new(),
exclude_regex: Vec::new(), exclude_regex: Vec::new(),
log_target: Cow::Borrowed(module_path!()), log_target: Cow::Borrowed(module_path!()),
log_level: Level::Info,
})) }))
} }
@ -139,6 +141,23 @@ impl Logger {
self self
} }
/// Sets the log level to `level`.
///
/// By default, the log level is `Level::Info`.
///
/// # Examples
/// Using `.log_level(Level::Debug)` would have this effect on request logs:
/// ```diff
/// - [2015-10-21T07:28:00Z INFO actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
/// + [2015-10-21T07:28:00Z DEBUG actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
/// ^^^^^^
/// ```
pub fn log_level(mut self, level: log::Level) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap();
inner.log_level = level;
self
}
/// Register a function that receives a ServiceRequest and returns a String for use in the /// Register a function that receives a ServiceRequest and returns a String for use in the
/// log line. The label passed as the first argument should match a replacement substring in /// log line. The label passed as the first argument should match a replacement substring in
/// the logger format like `%{label}xi`. /// the logger format like `%{label}xi`.
@ -242,6 +261,7 @@ impl Default for Logger {
exclude: HashSet::new(), exclude: HashSet::new(),
exclude_regex: Vec::new(), exclude_regex: Vec::new(),
log_target: Cow::Borrowed(module_path!()), log_target: Cow::Borrowed(module_path!()),
log_level: Level::Info,
})) }))
} }
} }
@ -312,6 +332,7 @@ where
format: None, format: None,
time: OffsetDateTime::now_utc(), time: OffsetDateTime::now_utc(),
log_target: Cow::Borrowed(""), log_target: Cow::Borrowed(""),
log_level: self.inner.log_level,
_phantom: PhantomData, _phantom: PhantomData,
} }
} else { } else {
@ -327,6 +348,7 @@ where
format: Some(format), format: Some(format),
time: now, time: now,
log_target: self.inner.log_target.clone(), log_target: self.inner.log_target.clone(),
log_level: self.inner.log_level,
_phantom: PhantomData, _phantom: PhantomData,
} }
} }
@ -344,6 +366,7 @@ pin_project! {
time: OffsetDateTime, time: OffsetDateTime,
format: Option<Format>, format: Option<Format>,
log_target: Cow<'static, str>, log_target: Cow<'static, str>,
log_level: Level,
_phantom: PhantomData<B>, _phantom: PhantomData<B>,
} }
} }
@ -390,6 +413,7 @@ where
let time = *this.time; let time = *this.time;
let format = this.format.take(); let format = this.format.take();
let log_target = this.log_target.clone(); let log_target = this.log_target.clone();
let log_level = *this.log_level;
Poll::Ready(Ok(res.map_body(move |_, body| StreamLog { Poll::Ready(Ok(res.map_body(move |_, body| StreamLog {
body, body,
@ -397,6 +421,7 @@ where
format, format,
size: 0, size: 0,
log_target, log_target,
log_level,
}))) })))
} }
} }
@ -409,6 +434,7 @@ pin_project! {
size: usize, size: usize,
time: OffsetDateTime, time: OffsetDateTime,
log_target: Cow<'static, str>, log_target: Cow<'static, str>,
log_level: Level
} }
impl<B> PinnedDrop for StreamLog<B> { impl<B> PinnedDrop for StreamLog<B> {
@ -421,8 +447,9 @@ pin_project! {
Ok(()) Ok(())
}; };
log::info!( log::log!(
target: this.log_target.as_ref(), target: this.log_target.as_ref(),
this.log_level,
"{}", FormatDisplay(&render) "{}", FormatDisplay(&render)
); );
} }
@ -622,9 +649,9 @@ impl FormatText {
FormatText::ResponseHeader(ref name) => { FormatText::ResponseHeader(ref name) => {
let s = if let Some(val) = res.headers().get(name) { let s = if let Some(val) = res.headers().get(name) {
val.to_str().unwrap_or("-") String::from_utf8_lossy(val.as_bytes()).into_owned()
} else { } else {
"-" "-".to_owned()
}; };
*self = FormatText::Str(s.to_string()) *self = FormatText::Str(s.to_string())
} }
@ -666,11 +693,11 @@ impl FormatText {
FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()), FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()),
FormatText::RequestHeader(ref name) => { FormatText::RequestHeader(ref name) => {
let s = if let Some(val) = req.headers().get(name) { let s = if let Some(val) = req.headers().get(name) {
val.to_str().unwrap_or("-") String::from_utf8_lossy(val.as_bytes()).into_owned()
} else { } else {
"-" "-".to_owned()
}; };
*self = FormatText::Str(s.to_string()); *self = FormatText::Str(s);
} }
FormatText::RemoteAddr => { FormatText::RemoteAddr => {
let s = if let Some(peer) = req.connection_info().peer_addr() { let s = if let Some(peer) = req.connection_info().peer_addr() {

View File

@ -264,8 +264,10 @@ impl HttpRequest {
/// ///
/// For expanded client connection information, use [`connection_info`] instead. /// For expanded client connection information, use [`connection_info`] instead.
/// ///
/// Will only return None when called in unit tests unless [`TestRequest::peer_addr`] is used. /// Will only return `None` when server is listening on [UDS socket] or when called in unit
/// tests unless [`TestRequest::peer_addr`] is used.
/// ///
/// [UDS socket]: crate::HttpServer::bind_uds
/// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr /// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr
/// [`connection_info`]: Self::connection_info /// [`connection_info`]: Self::connection_info
#[inline] #[inline]

View File

@ -11,7 +11,7 @@ use futures_core::Stream;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
body::{BodyStream, BoxBody, MessageBody}, body::{BodyStream, BoxBody, MessageBody, SizedStream},
dev::Extensions, dev::Extensions,
error::{Error, JsonPayloadError}, error::{Error, JsonPayloadError},
http::{ http::{
@ -318,13 +318,35 @@ impl HttpResponseBuilder {
/// Set a streaming body and build the `HttpResponse`. /// Set a streaming body and build the `HttpResponse`.
/// ///
/// `HttpResponseBuilder` can not be used after this call. /// `HttpResponseBuilder` can not be used after this call.
///
/// If `Content-Type` is not set, then it is automatically set to `application/octet-stream`.
///
/// If `Content-Length` is set, then [`no_chunking()`](Self::no_chunking) is automatically called.
#[inline] #[inline]
pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse
where where
S: Stream<Item = Result<Bytes, E>> + 'static, S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<BoxError> + 'static, E: Into<BoxError> + 'static,
{ {
self.body(BodyStream::new(stream)) // Set mime type to application/octet-stream if it is not set
if let Some(parts) = self.inner() {
if !parts.headers.contains_key(header::CONTENT_TYPE) {
self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
}
}
let content_length = self
.inner()
.and_then(|parts| parts.headers.get(header::CONTENT_LENGTH))
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<u64>().ok());
if let Some(len) = content_length {
self.no_chunking(len);
self.body(SizedStream::new(len, stream))
} else {
self.body(BodyStream::new(stream))
}
} }
/// Set a JSON body and build the `HttpResponse`. /// Set a JSON body and build the `HttpResponse`.

View File

@ -6,7 +6,8 @@ use crate::{HttpResponse, HttpResponseBuilder};
macro_rules! static_resp { macro_rules! static_resp {
($name:ident, $status:expr) => { ($name:ident, $status:expr) => {
#[allow(non_snake_case, missing_docs)] #[allow(non_snake_case)]
#[doc = concat!("Creates a new response builder with the status code `", stringify!($status), "`.")]
pub fn $name() -> HttpResponseBuilder { pub fn $name() -> HttpResponseBuilder {
HttpResponseBuilder::new($status) HttpResponseBuilder::new($status)
} }

View File

@ -1,6 +1,8 @@
use std::{ use std::{
any::Any, any::Any,
cmp, fmt, io, cmp, fmt,
future::Future,
io,
marker::PhantomData, marker::PhantomData,
net, net,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@ -29,6 +31,7 @@ struct Config {
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_request_timeout: Duration, client_request_timeout: Duration,
client_disconnect_timeout: Duration, client_disconnect_timeout: Duration,
h1_allow_half_closed: bool,
#[allow(dead_code)] // only dead when no TLS features are enabled #[allow(dead_code)] // only dead when no TLS features are enabled
tls_handshake_timeout: Option<Duration>, tls_handshake_timeout: Option<Duration>,
} }
@ -64,6 +67,7 @@ struct Config {
/// .await /// .await
/// } /// }
/// ``` /// ```
#[must_use]
pub struct HttpServer<F, I, S, B> pub struct HttpServer<F, I, S, B>
where where
F: Fn() -> I + Send + Clone + 'static, F: Fn() -> I + Send + Clone + 'static,
@ -113,6 +117,7 @@ where
keep_alive: KeepAlive::default(), keep_alive: KeepAlive::default(),
client_request_timeout: Duration::from_secs(5), client_request_timeout: Duration::from_secs(5),
client_disconnect_timeout: Duration::from_secs(1), client_disconnect_timeout: Duration::from_secs(1),
h1_allow_half_closed: true,
tls_handshake_timeout: None, tls_handshake_timeout: None,
})), })),
backlog: 1024, backlog: 1024,
@ -254,6 +259,18 @@ where
self.client_disconnect_timeout(Duration::from_millis(dur)) self.client_disconnect_timeout(Duration::from_millis(dur))
} }
/// Sets whether HTTP/1 connections should support half-closures.
///
/// Clients can choose to shutdown their writer-side of the connection after completing their
/// request and while waiting for the server response. Setting this to `false` will cause the
/// server to abort the connection handling as soon as it detects an EOF from the client.
///
/// The default behavior is to allow, i.e. `true`
pub fn h1_allow_half_closed(self, allow: bool) -> Self {
self.config.lock().unwrap().h1_allow_half_closed = allow;
self
}
/// Sets function that will be called once before each connection is handled. /// Sets function that will be called once before each connection is handled.
/// ///
/// It will receive a `&std::any::Any`, which contains underlying connection type and an /// It will receive a `&std::any::Any`, which contains underlying connection type and an
@ -272,19 +289,12 @@ where
/// - `actix_web::rt::net::TcpStream` when no encryption is used. /// - `actix_web::rt::net::TcpStream` when no encryption is used.
/// ///
/// See the `on_connect` example for additional details. /// See the `on_connect` example for additional details.
pub fn on_connect<CB>(self, f: CB) -> HttpServer<F, I, S, B> pub fn on_connect<CB>(mut self, f: CB) -> HttpServer<F, I, S, B>
where where
CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static, CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static,
{ {
HttpServer { self.on_connect_fn = Some(Arc::new(f));
factory: self.factory, self
config: self.config,
backlog: self.backlog,
sockets: self.sockets,
builder: self.builder,
on_connect_fn: Some(Arc::new(f)),
_phantom: PhantomData,
}
} }
/// Sets server host name. /// Sets server host name.
@ -312,6 +322,37 @@ where
self self
} }
/// Specify shutdown signal from a future.
///
/// Using this method will prevent OS signal handlers being set up.
///
/// Typically, a `CancellationToken` will be used, but any future _can_ be.
///
/// # Examples
///
/// ```no_run
/// use actix_web::{App, HttpServer};
/// use tokio_util::sync::CancellationToken;
///
/// # #[actix_web::main]
/// # async fn main() -> std::io::Result<()> {
/// let stop_signal = CancellationToken::new();
///
/// HttpServer::new(move || App::new())
/// .shutdown_signal(stop_signal.cancelled_owned())
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// # }
/// ```
pub fn shutdown_signal<Fut>(mut self, shutdown_signal: Fut) -> Self
where
Fut: Future<Output = ()> + Send + 'static,
{
self.builder = self.builder.shutdown_signal(shutdown_signal);
self
}
/// Sets timeout for graceful worker shutdown of workers. /// Sets timeout for graceful worker shutdown of workers.
/// ///
/// After receiving a stop signal, workers have this much time to finish serving requests. /// After receiving a stop signal, workers have this much time to finish serving requests.
@ -531,6 +572,7 @@ where
.keep_alive(cfg.keep_alive) .keep_alive(cfg.keep_alive)
.client_request_timeout(cfg.client_request_timeout) .client_request_timeout(cfg.client_request_timeout)
.client_disconnect_timeout(cfg.client_disconnect_timeout) .client_disconnect_timeout(cfg.client_disconnect_timeout)
.h1_allow_half_closed(cfg.h1_allow_half_closed)
.local_addr(addr); .local_addr(addr);
if let Some(handler) = on_connect_fn.clone() { if let Some(handler) = on_connect_fn.clone() {
@ -575,6 +617,7 @@ where
.keep_alive(cfg.keep_alive) .keep_alive(cfg.keep_alive)
.client_request_timeout(cfg.client_request_timeout) .client_request_timeout(cfg.client_request_timeout)
.client_disconnect_timeout(cfg.client_disconnect_timeout) .client_disconnect_timeout(cfg.client_disconnect_timeout)
.h1_allow_half_closed(cfg.h1_allow_half_closed)
.local_addr(addr); .local_addr(addr);
if let Some(handler) = on_connect_fn.clone() { if let Some(handler) = on_connect_fn.clone() {
@ -650,6 +693,7 @@ where
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -701,6 +745,7 @@ where
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -767,6 +812,7 @@ where
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -833,6 +879,7 @@ where
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -882,6 +929,7 @@ where
let factory = self.factory.clone(); let factory = self.factory.clone();
let cfg = Arc::clone(&self.config); let cfg = Arc::clone(&self.config);
let addr = lst.local_addr().unwrap(); let addr = lst.local_addr().unwrap();
self.sockets.push(Socket { self.sockets.push(Socket {
addr, addr,
scheme: "https", scheme: "https",
@ -899,6 +947,7 @@ where
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.client_disconnect_timeout(c.client_disconnect_timeout) .client_disconnect_timeout(c.client_disconnect_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.local_addr(addr); .local_addr(addr);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -967,6 +1016,7 @@ where
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.client_disconnect_timeout(c.client_disconnect_timeout) .client_disconnect_timeout(c.client_disconnect_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.finish(map_config(fac, move |_| config.clone())), .finish(map_config(fac, move |_| config.clone())),
) )
}, },
@ -986,6 +1036,7 @@ where
let factory = self.factory.clone(); let factory = self.factory.clone();
let socket_addr = let socket_addr =
net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080);
self.sockets.push(Socket { self.sockets.push(Socket {
scheme: "http", scheme: "http",
addr: socket_addr, addr: socket_addr,
@ -1007,6 +1058,7 @@ where
let mut svc = HttpService::build() let mut svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
if let Some(handler) = on_connect_fn.clone() { if let Some(handler) = on_connect_fn.clone() {
@ -1073,10 +1125,7 @@ fn bind_addrs(addrs: impl net::ToSocketAddrs, backlog: u32) -> io::Result<Vec<ne
} else if let Some(err) = err.take() { } else if let Some(err) = err.take() {
Err(err) Err(err)
} else { } else {
Err(io::Error::new( Err(io::Error::other("Could not bind to address"))
io::ErrorKind::Other,
"Can not bind to address.",
))
} }
} }

View File

@ -238,7 +238,7 @@ where
match res { match res {
Ok(bytes) => { Ok(bytes) => {
let fallback = bytes.clone(); let fallback = bytes.clone();
let left = L::from_request(this.req, &mut payload_from_bytes(bytes)); let left = L::from_request(this.req, &mut dev::Payload::from(bytes));
EitherExtractState::Left { left, fallback } EitherExtractState::Left { left, fallback }
} }
Err(err) => break Err(EitherExtractError::Bytes(err)), Err(err) => break Err(EitherExtractError::Bytes(err)),
@ -251,7 +251,7 @@ where
Err(left_err) => { Err(left_err) => {
let right = R::from_request( let right = R::from_request(
this.req, this.req,
&mut payload_from_bytes(mem::take(fallback)), &mut dev::Payload::from(mem::take(fallback)),
); );
EitherExtractState::Right { EitherExtractState::Right {
left_err: Some(left_err), left_err: Some(left_err),
@ -276,12 +276,6 @@ where
} }
} }
fn payload_from_bytes(bytes: Bytes) -> dev::Payload {
let (_, mut h1_payload) = actix_http::h1::Payload::create(true);
h1_payload.unread_data(bytes);
dev::Payload::from(h1_payload)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -616,7 +616,7 @@ mod tests {
} }
)); ));
let (req, mut pl) = TestRequest::default() let (mut req, mut pl) = TestRequest::default()
.insert_header(( .insert_header((
header::CONTENT_TYPE, header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"), header::HeaderValue::from_static("application/json"),
@ -624,6 +624,7 @@ mod tests {
.set_payload(Bytes::from_static(&[0u8; 1000])) .set_payload(Bytes::from_static(&[0u8; 1000]))
.to_http_parts(); .to_http_parts();
req.head_mut().headers_mut().remove(header::CONTENT_LENGTH);
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true) let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true)
.limit(100) .limit(100)
.await; .await;

View File

@ -11,7 +11,7 @@ mod query;
mod readlines; mod readlines;
pub use self::{ pub use self::{
either::Either, either::{Either, EitherExtractError},
form::{Form, FormConfig, UrlEncoded}, form::{Form, FormConfig, UrlEncoded},
header::Header, header::Header,
html::Html, html::Html,

View File

@ -38,7 +38,7 @@ use crate::{
/// ///
/// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used /// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used
/// later in a request handler to access the matched value for that segment. This is done by looking /// later in a request handler to access the matched value for that segment. This is done by looking
/// up the identifier in the `Path` object returned by [`HttpRequest.match_info()`] method. /// up the identifier in the `Path` object returned by [`HttpRequest::match_info()`](crate::HttpRequest::match_info) method.
/// ///
/// By default, each segment matches the regular expression `[^{}/]+`. /// By default, each segment matches the regular expression `[^{}/]+`.
/// ///

View File

@ -0,0 +1,115 @@
use std::{
pin::Pin,
task::{Context, Poll},
};
use actix_web::{
http::header::{self, HeaderValue},
HttpResponse,
};
use bytes::Bytes;
use futures_core::Stream;
struct FixedSizeStream {
data: Vec<u8>,
yielded: bool,
}
impl FixedSizeStream {
fn new(size: usize) -> Self {
Self {
data: vec![0u8; size],
yielded: false,
}
}
}
impl Stream for FixedSizeStream {
type Item = Result<Bytes, std::io::Error>;
fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.yielded {
Poll::Ready(None)
} else {
self.yielded = true;
let data = std::mem::take(&mut self.data);
Poll::Ready(Some(Ok(Bytes::from(data))))
}
}
}
#[actix_rt::test]
async fn test_streaming_response_with_content_length() {
let stream = FixedSizeStream::new(100);
let resp = HttpResponse::Ok()
.append_header((header::CONTENT_LENGTH, "100"))
.streaming(stream);
assert_eq!(
resp.headers().get(header::CONTENT_LENGTH),
Some(&HeaderValue::from_static("100")),
"Content-Length should be preserved when explicitly set"
);
let has_chunked = resp
.headers()
.get(header::TRANSFER_ENCODING)
.map(|v| v.to_str().unwrap_or(""))
.unwrap_or("")
.contains("chunked");
assert!(
!has_chunked,
"chunked should not be used when Content-Length is provided"
);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("application/octet-stream")),
"Content-Type should default to application/octet-stream"
);
}
#[actix_rt::test]
async fn test_streaming_response_default_content_type() {
let stream = FixedSizeStream::new(50);
let resp = HttpResponse::Ok().streaming(stream);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("application/octet-stream")),
"Content-Type should default to application/octet-stream"
);
}
#[actix_rt::test]
async fn test_streaming_response_user_defined_content_type() {
let stream = FixedSizeStream::new(25);
let resp = HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "text/plain"))
.streaming(stream);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain")),
"User-defined Content-Type should be preserved"
);
}
#[actix_rt::test]
async fn test_streaming_response_empty_stream() {
let stream = FixedSizeStream::new(0);
let resp = HttpResponse::Ok()
.append_header((header::CONTENT_LENGTH, "0"))
.streaming(stream);
assert_eq!(
resp.headers().get(header::CONTENT_LENGTH),
Some(&HeaderValue::from_static("0")),
"Content-Length 0 should be preserved for empty streams"
);
}

View File

@ -2,10 +2,25 @@
## Unreleased ## Unreleased
- Update `brotli` dependency to `7`. ## 3.8.1
- Fix a bug where `GO_AWAY` errors did not stop connections from returning to the pool.
## 3.8.0
- Add `hickory-dns` crate feature (off-by-default).
- The `trust-dns` crate feature now delegates DNS resolution to `hickory-dns`.
## 3.7.0
- Update `brotli` dependency to `8`.
## 3.6.0
- Prevent panics on connection pool drop when Tokio runtime is shutdown early. - Prevent panics on connection pool drop when Tokio runtime is shutdown early.
- Minimum supported Rust version (MSRV) is now 1.75.
- Do not send `Host` header on HTTP/2 requests, as it is not required, and some web servers may reject it. - Do not send `Host` header on HTTP/2 requests, as it is not required, and some web servers may reject it.
- Update `brotli` dependency to `7`.
- Minimum supported Rust version (MSRV) is now 1.75.
## 3.5.1 ## 3.5.1

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.5.1" version = "3.8.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP and WebSocket client library" description = "Async HTTP and WebSocket client library"
keywords = ["actix", "http", "framework", "async", "web"] keywords = ["actix", "http", "framework", "async", "web"]
@ -16,7 +16,6 @@ license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [ features = [
"cookies", "cookies",
"openssl", "openssl",
@ -83,8 +82,10 @@ compress-zstd = ["actix-http/compress-zstd", "__compress"]
# Cookie parsing and cookie jar # Cookie parsing and cookie jar
cookies = ["dep:cookie"] cookies = ["dep:cookie"]
# Use `trust-dns-resolver` crate as DNS resolver # Use `hickory-dns-resolver` crate as DNS resolver
trust-dns = ["trust-dns-resolver"] hickory-dns = ["dep:hickory-resolver"]
# Use `trust-dns-resolver` crate as DNS resolver (deprecated, use `hickory-dns`)
trust-dns = ["hickory-dns"]
# Internal (PRIVATE!) features used to aid testing and checking feature status. # Internal (PRIVATE!) features used to aid testing and checking feature status.
# Don't rely on these whatsoever. They may disappear at anytime. # Don't rely on these whatsoever. They may disappear at anytime.
@ -97,9 +98,9 @@ dangerous-h2c = []
[dependencies] [dependencies]
actix-codec = "0.5" actix-codec = "0.5"
actix-service = "2" actix-http = { version = "3.10", features = ["http2", "ws"] }
actix-http = { version = "3.7", features = ["http2", "ws"] }
actix-rt = { version = "2.1", default-features = false } actix-rt = { version = "2.1", default-features = false }
actix-service = "2"
actix-tls = { version = "3.4", features = ["connect", "uri"] } actix-tls = { version = "3.4", features = ["connect", "uri"] }
actix-utils = "3" actix-utils = "3"
@ -109,10 +110,10 @@ cfg-if = "1"
derive_more = { version = "2", features = ["display", "error", "from"] } derive_more = { version = "2", features = ["display", "error", "from"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.26" h2 = "0.3.27"
http = "0.2.7" http = "0.2.7"
itoa = "1" itoa = "1"
log =" 0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2" pin-project-lite = "0.2"
@ -120,7 +121,7 @@ rand = "0.9"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
cookie = { version = "0.16", features = ["percent-encode"], optional = true } cookie = { version = "0.16", features = ["percent-encode"], optional = true }
@ -130,7 +131,7 @@ tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true, featu
tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true } tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true }
tls-rustls-0_23 = { package = "rustls", version = "0.23", optional = true, default-features = false } tls-rustls-0_23 = { package = "rustls", version = "0.23", optional = true, default-features = false }
trust-dns-resolver = { version = "0.23", optional = true } hickory-resolver = { version = "0.25", optional = true, features = ["system-config", "tokio"] }
[dev-dependencies] [dev-dependencies]
actix-http = { version = "3.7", features = ["openssl"] } actix-http = { version = "3.7", features = ["openssl"] }
@ -141,15 +142,15 @@ actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23"] }
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4", features = ["openssl"] } actix-web = { version = "4", features = ["openssl"] }
brotli = "7" brotli = "8"
const-str = "0.5" const-str = "0.5" # TODO(MSRV 1.77): update to 0.6
env_logger = "0.11" env_logger = "0.11"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false }
static_assertions = "1.1" static_assertions = "1.1"
rcgen = "0.13" rcgen = "0.13"
rustls-pemfile = "2" rustls-pemfile = "2"
tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.38.2", features = ["rt-multi-thread", "macros"] }
zstd = "0.13" zstd = "0.13"
tls-rustls-0_23 = { package = "rustls", version = "0.23" } # add rustls 0.23 with default features to make aws_lc_rs work in tests tls-rustls-0_23 = { package = "rustls", version = "0.23" } # add rustls 0.23 with default features to make aws_lc_rs work in tests

View File

@ -5,16 +5,16 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.5.1)](https://docs.rs/awc/3.5.1) [![Documentation](https://docs.rs/awc/badge.svg?version=3.8.1)](https://docs.rs/awc/3.8.1)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.5.1/status.svg)](https://deps.rs/crate/awc/3.5.1) [![Dependency Status](https://deps.rs/crate/awc/3.8.1/status.svg)](https://deps.rs/crate/awc/3.8.1)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
## Examples ## Examples
[Example project using TLS-enabled client →](https://github.com/actix/examples/tree/master/https-tls/awc-https) [Example project using TLS-enabled client →](https://github.com/actix/examples/tree/main/https-tls/awc-https)
Basic usage: Basic usage:

View File

@ -89,9 +89,9 @@ impl Connector<()> {
/// # Panics /// # Panics
/// ///
/// - When the `rustls-0_23-webpki-roots` or `rustls-0_23-native-roots` features are enabled /// - When the `rustls-0_23-webpki-roots` or `rustls-0_23-native-roots` features are enabled
/// and no default crypto provider has been loaded, this method will panic. /// and no default crypto provider has been loaded, this method will panic.
/// - When the `rustls-0_23-native-roots` or `rustls-0_22-native-roots` features are enabled /// - When the `rustls-0_23-native-roots` or `rustls-0_22-native-roots` features are enabled
/// and the runtime system has no native root certificates, this method will panic. /// and the runtime system has no native root certificates, this method will panic.
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)] #[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
pub fn new() -> Connector< pub fn new() -> Connector<
impl Service< impl Service<
@ -1037,7 +1037,7 @@ where
} }
} }
#[cfg(not(feature = "trust-dns"))] #[cfg(not(feature = "hickory-dns"))]
mod resolver { mod resolver {
use super::*; use super::*;
@ -1046,24 +1046,25 @@ mod resolver {
} }
} }
#[cfg(feature = "trust-dns")] #[cfg(feature = "hickory-dns")]
mod resolver { mod resolver {
use std::{cell::RefCell, net::SocketAddr}; use std::{cell::OnceCell, net::SocketAddr};
use actix_tls::connect::Resolve; use actix_tls::connect::Resolve;
use trust_dns_resolver::{ use hickory_resolver::{
config::{ResolverConfig, ResolverOpts}, config::{ResolverConfig, ResolverOpts},
name_server::TokioConnectionProvider,
system_conf::read_system_conf, system_conf::read_system_conf,
TokioAsyncResolver, TokioResolver,
}; };
use super::*; use super::*;
pub(super) fn resolver() -> Resolver { pub(super) fn resolver() -> Resolver {
// new type for impl Resolve trait for TokioAsyncResolver. // new type for impl Resolve trait for TokioAsyncResolver.
struct TrustDnsResolver(TokioAsyncResolver); struct HickoryDnsResolver(TokioResolver);
impl Resolve for TrustDnsResolver { impl Resolve for HickoryDnsResolver {
fn lookup<'a>( fn lookup<'a>(
&'a self, &'a self,
host: &'a str, host: &'a str,
@ -1085,34 +1086,29 @@ mod resolver {
// resolver struct is cached in thread local so new clients can reuse the existing instance // resolver struct is cached in thread local so new clients can reuse the existing instance
thread_local! { thread_local! {
static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = const { RefCell::new(None) }; static HICKORY_DNS_RESOLVER: OnceCell<Resolver> = const { OnceCell::new() };
} }
// get from thread local or construct a new trust-dns resolver. // get from thread local or construct a new hickory dns resolver.
TRUST_DNS_RESOLVER.with(|local| { HICKORY_DNS_RESOLVER.with(|local| {
let resolver = local.borrow().as_ref().map(Clone::clone); local
.get_or_init(|| {
match resolver {
Some(resolver) => resolver,
None => {
let (cfg, opts) = match read_system_conf() { let (cfg, opts) = match read_system_conf() {
Ok((cfg, opts)) => (cfg, opts), Ok((cfg, opts)) => (cfg, opts),
Err(err) => { Err(err) => {
log::error!("Trust-DNS can not load system config: {err}"); log::error!("Hickory DNS can not load system config: {err}");
(ResolverConfig::default(), ResolverOpts::default()) (ResolverConfig::default(), ResolverOpts::default())
} }
}; };
let resolver = TokioAsyncResolver::tokio(cfg, opts); let resolver =
TokioResolver::builder_with_config(cfg, TokioConnectionProvider::default())
.with_options(opts)
.build();
// box trust dns resolver and put it in thread local. Resolver::custom(HickoryDnsResolver(resolver))
let resolver = Resolver::custom(TrustDnsResolver(resolver)); })
*local.borrow_mut() = Some(resolver.clone()); .clone()
resolver
}
}
}) })
} }
} }

View File

@ -107,7 +107,7 @@ where
let res = poll_fn(|cx| io.poll_ready(cx)).await; let res = poll_fn(|cx| io.poll_ready(cx)).await;
if let Err(err) = res { if let Err(err) = res {
io.on_release(err.is_io()); io.on_release(err.is_io() || err.is_go_away());
return Err(SendRequestError::from(err)); return Err(SendRequestError::from(err));
} }
@ -121,7 +121,7 @@ where
fut.await.map_err(SendRequestError::from)? fut.await.map_err(SendRequestError::from)?
} }
Err(err) => { Err(err) => {
io.on_release(err.is_io()); io.on_release(err.is_io() || err.is_go_away());
return Err(err.into()); return Err(err.into());
} }
}; };

View File

@ -179,9 +179,8 @@ where
.acquire_owned() .acquire_owned()
.await .await
.map_err(|_| { .map_err(|_| {
ConnectError::Io(io::Error::new( ConnectError::Io(io::Error::other(
io::ErrorKind::Other, "Failed to acquire semaphore on client connection pool",
"failed to acquire semaphore on client connection pool",
)) ))
})?; })?;

View File

@ -108,7 +108,7 @@
)] )]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
pub use actix_http::body; pub use actix_http::body;
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]

View File

@ -65,9 +65,7 @@ impl TestResponse {
/// Set response's payload /// Set response's payload
pub fn set_payload<B: Into<Bytes>>(mut self, data: B) -> Self { pub fn set_payload<B: Into<Bytes>>(mut self, data: B) -> Self {
let (_, mut payload) = h1::Payload::create(true); self.payload = Some(Payload::from(data.into()));
payload.unread_data(data.into());
self.payload = Some(payload.into());
self self
} }

45
deny.toml Normal file
View File

@ -0,0 +1,45 @@
[licenses]
confidence-threshold = 0.90
allow = [
"Apache-2.0",
"MIT",
"Unicode-3.0",
"ISC",
"CDLA-Permissive-2.0",
"BSD-3-Clause",
"Zlib",
"OpenSSL",
"MPL-2.0"
]
private = { ignore = true }
# FIXME: old rustls introduces old ring which is not set license field properly.
[[licenses.clarify]]
crate = "ring"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 }
]
# FIXME: webpki is almost unmaintained and is not set license field properly.
# rustls has its own fork now so removing old rustls should resolve the issue.
[[licenses.clarify]]
crate = "webpki"
expression = "ISC"
license-files = [
{ path = "LICENSE", hash = 0x001c7e6c }
]
[bans]
multiple-versions = "allow"
[bans.build]
executables = "deny"
[advisories]
# because of old rustls support:
ignore = [
"RUSTSEC-2024-0336",
"RUSTSEC-2025-0009",
"RUSTSEC-2025-0010"
]

View File

@ -1,18 +1,25 @@
_list: _list:
@just --list @just --list
toolchain := ""
# Format workspace. # Format workspace.
fmt: fmt:
just --unstable --fmt just --unstable --fmt
cargo +nightly fmt cargo +nightly fmt
fd --hidden --type=file --extension=md --extension=yml --exec-batch npx -y prettier --write fd --hidden --type=file --extension=md --extension=yml --exec-batch npx -y prettier --write
# Downgrade dev-dependencies necessary to run MSRV checks/tests. # Downgrade dependencies necessary to run MSRV checks/tests.
[private] [private]
downgrade-for-msrv: downgrade-for-msrv:
cargo update -p=parse-size --precise=1.0.0 cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0
cargo update -p=clap --precise=4.4.18 cargo {{ toolchain }} update -p=rayon --precise=1.10.0 # next ver: 1.80.0
cargo update -p=divan --precise=0.1.15 cargo {{ toolchain }} update -p=rayon-core --precise=1.12.1 # next ver: 1.80.0
cargo {{ toolchain }} update -p=half --precise=2.4.1 # next ver: 1.81.0
cargo {{ toolchain }} update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0
cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
cargo {{ toolchain }} update -p=zerofrom --precise=0.1.5 # next ver: 1.81.0
cargo {{ toolchain }} update -p=time --precise=0.3.41 # next ver: 1.81.0
msrv := ``` msrv := ```
cargo metadata --format-version=1 \ cargo metadata --format-version=1 \
@ -37,48 +44,55 @@ check-min:
check-default: check-default:
cargo hack --workspace check cargo hack --workspace check
# Run Clippy over workspace. # Check workspace.
check toolchain="": && (clippy toolchain) check: && clippy
fd --hidden --type=file --extension=md --extension=yml --exec-batch npx -y prettier --check
# Run Clippy over workspace. # Run Clippy over workspace.
clippy toolchain="": clippy:
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }} cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
# Test workspace using MSRV. # Run Clippy over workspace using MSRV.
test-msrv: downgrade-for-msrv (test msrv_rustup) clippy-msrv: downgrade-for-msrv
@just toolchain={{ msrv_rustup }} clippy
# Test workspace code. # Test workspace code.
test toolchain="": test:
cargo {{ toolchain }} test --lib --tests -p=actix-web-codegen --all-features cargo {{ toolchain }} test --lib --tests -p=actix-web-codegen --all-features
cargo {{ toolchain }} test --lib --tests -p=actix-multipart-derive --all-features cargo {{ toolchain }} test --lib --tests -p=actix-multipart-derive --all-features
cargo {{ toolchain }} nextest run --no-tests=warn -p=actix-router --no-default-features cargo {{ toolchain }} nextest run --no-tests=warn -p=actix-router --no-default-features
cargo {{ toolchain }} nextest run --no-tests=warn --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)" cargo {{ toolchain }} nextest run --no-tests=warn --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)"
# Test workspace using MSRV.
test-msrv: downgrade-for-msrv
@just toolchain={{ msrv_rustup }} test
# Test workspace docs. # Test workspace docs.
test-docs toolchain="": && doc test-docs: && doc
cargo {{ toolchain }} test --doc --workspace {{ all_crate_features }} --no-fail-fast -- --nocapture cargo {{ toolchain }} test --doc --workspace {{ all_crate_features }} --no-fail-fast -- --nocapture
# Test workspace. # Test workspace.
test-all toolchain="": (test toolchain) (test-docs toolchain) test-all: test test-docs
# Test workspace and collect coverage info. # Test workspace and collect coverage info.
[private] [private]
test-coverage toolchain="": test-coverage:
cargo {{ toolchain }} llvm-cov nextest --no-tests=warn --no-report {{ all_crate_features }} cargo {{ toolchain }} llvm-cov nextest --no-tests=warn --no-report {{ all_crate_features }}
cargo {{ toolchain }} llvm-cov --doc --no-report {{ all_crate_features }} cargo {{ toolchain }} llvm-cov --doc --no-report {{ all_crate_features }}
# Test workspace and generate Codecov report. # Test workspace and generate Codecov report.
test-coverage-codecov toolchain="": (test-coverage toolchain) test-coverage-codecov: test-coverage
cargo {{ toolchain }} llvm-cov report --doctests --codecov --output-path=codecov.json cargo {{ toolchain }} llvm-cov report --doctests --codecov --output-path=codecov.json
# Test workspace and generate LCOV report. # Test workspace and generate LCOV report.
test-coverage-lcov toolchain="": (test-coverage toolchain) test-coverage-lcov: test-coverage
cargo {{ toolchain }} llvm-cov report --doctests --lcov --output-path=lcov.info cargo {{ toolchain }} llvm-cov report --doctests --lcov --output-path=lcov.info
# Document crates in workspace. # Document crates in workspace.
# FIXME: Re-add `RUSTDOCFLAGS="--cfg=docsrs -Dwarnings"` once crypto-related crates are updated.
doc *args: && doc-set-workspace-crates doc *args: && doc-set-workspace-crates
rm -f "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js" rm -f "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js"
RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --workspace {{ all_crate_features }} {{ args }} cargo +nightly doc --workspace {{ all_crate_features }} {{ args }}
[private] [private]
doc-set-workspace-crates: doc-set-workspace-crates:

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
# developed on macOS and probably doesn't work on Linux yet due to minor # developed on macOS and probably doesn't work on Linux yet due to minor
# differences in flags on sed # differences in flags on sed

View File

@ -1,38 +0,0 @@
#!/bin/sh
# run tests matching what CI does for non-linux feature sets
set -x
EXIT=0
save_exit_code() {
eval $@
local CMD_EXIT=$?
[ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT
}
save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture
save_exit_code cargo test --workspace --doc
if [ "$EXIT" = "0" ]; then
PASSED="All tests passed!"
if [ "$(command -v figlet)" ]; then
figlet "$PASSED"
else
echo "$PASSED"
fi
fi
exit $EXIT

25
scripts/publish Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -Euo pipefail
for dir in $@; do
cd "$dir"
cargo publish --dry-run
read -p "Look okay? "
read -p "Sure? "
cargo publish
if [ $? -ne 0 ]; then
echo
read -p "Was the above error caused by cyclic dev-deps? Choosing yes will publish without a git backreference. (y/N) " publish_no_dev_deps
if [[ "$publish_no_dev_deps" == "y" || "$publish_no_dev_deps" == "Y" ]]; then
cargo hack --no-dev-deps publish --allow-dirty
fi
fi
cd ..
done