mirror of https://github.com/fafhrd91/actix-web
Compare commits
107 Commits
f3b64b0f73
...
a3e428f7fb
| Author | SHA1 | Date |
|---|---|---|
|
|
a3e428f7fb | |
|
|
fe890a73b2 | |
|
|
d119500f93 | |
|
|
a3f95ee1ef | |
|
|
e1da110e92 | |
|
|
219b988300 | |
|
|
9cf3521390 | |
|
|
8b103199ea | |
|
|
d5fae3ecc2 | |
|
|
e6958cc677 | |
|
|
3dd42ee13e | |
|
|
627af8ce57 | |
|
|
97e701721b | |
|
|
9e1118cfdd | |
|
|
a769f6636c | |
|
|
2967e4af9f | |
|
|
832f893bc7 | |
|
|
55ebece1fa | |
|
|
6ecfcb6418 | |
|
|
856263cfd2 | |
|
|
6d9e47720f | |
|
|
35cc89a5a8 | |
|
|
c8c00137b0 | |
|
|
e8f0e8b1c4 | |
|
|
df016be17c | |
|
|
d36a5b49d8 | |
|
|
832a1da262 | |
|
|
25963dbdf1 | |
|
|
4df9953c86 | |
|
|
016841137e | |
|
|
fde7934243 | |
|
|
429ff82a4b | |
|
|
28a3a44e52 | |
|
|
91b62b950d | |
|
|
8935f7a4e3 | |
|
|
df2c6b3208 | |
|
|
d580cf0709 | |
|
|
0f0ceb5a73 | |
|
|
5c6a29f435 | |
|
|
b200a45c8e | |
|
|
89fea1f5ee | |
|
|
5d0cf52294 | |
|
|
41d0176c1d | |
|
|
6e6c2491c6 | |
|
|
a06cdf3754 | |
|
|
25a12177af | |
|
|
f3ce1bc45f | |
|
|
7f02a7e41a | |
|
|
a619022c95 | |
|
|
1d6e978d82 | |
|
|
d43738306c | |
|
|
62d6d5863e | |
|
|
f4ee39cdd7 | |
|
|
f54254d0bd | |
|
|
78ee652140 | |
|
|
03e3e344cc | |
|
|
d1acaa3b3a | |
|
|
4febfa4446 | |
|
|
ae354b8edc | |
|
|
7726669f9f | |
|
|
65c5a043d7 | |
|
|
c85e058f5d | |
|
|
98d7d0b46b | |
|
|
4966a54e05 | |
|
|
00b0f8f700 | |
|
|
3c2907da41 | |
|
|
5041cd1c65 | |
|
|
d3c46537b3 | |
|
|
8996198f2c | |
|
|
68624ec63b | |
|
|
bcd0ffb016 | |
|
|
9fb6c13a1a | |
|
|
05cfef7f4b | |
|
|
8f3eb32a32 | |
|
|
ddd16ec9db | |
|
|
9c47a247fb | |
|
|
2536823e3b | |
|
|
e3f81d0643 | |
|
|
a84aee836b | |
|
|
1f08cb24c3 | |
|
|
1b49047086 | |
|
|
9fe033a963 | |
|
|
2ba69a1904 | |
|
|
c6352005f7 | |
|
|
aeb42486af | |
|
|
30f7df5853 | |
|
|
e3ae20de30 | |
|
|
5f6c84494a | |
|
|
37203c7fd8 | |
|
|
971bbc1468 | |
|
|
d08a85b470 | |
|
|
d94442246f | |
|
|
706636da67 | |
|
|
be6be9fbcc | |
|
|
cd71768509 | |
|
|
d5abb5ae45 | |
|
|
522f60fa8c | |
|
|
c65633b548 | |
|
|
351f7a17ed | |
|
|
254fcab19d | |
|
|
3c67d04cbe | |
|
|
b355603c99 | |
|
|
a81c05818f | |
|
|
aceff9d13f | |
|
|
1896ff2ec7 | |
|
|
0eef04ed0a | |
|
|
6e396bc352 |
|
|
@ -3,6 +3,6 @@ disallowed-names = [
|
|||
"e", # no single letter error bindings
|
||||
]
|
||||
disallowed-methods = [
|
||||
{ path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default" },
|
||||
{ path = "std::rc::Rc::default()", reason = "prefer explicit inner type 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ version: "0.2"
|
|||
words:
|
||||
- actix
|
||||
- addrs
|
||||
- ALPN
|
||||
- bytestring
|
||||
- httparse
|
||||
- msrv
|
||||
- MSRV
|
||||
- realip
|
||||
- rustls
|
||||
- rustup
|
||||
- serde
|
||||
- uring
|
||||
- webpki
|
||||
- zstd
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [robjtede]
|
||||
github: [robjtede, JohnTitor]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ name: Benchmark
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ name: CI (post-merge)
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -28,11 +28,11 @@ jobs:
|
|||
runs-on: ${{ matrix.target.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install nasm
|
||||
if: matrix.target.os == 'windows-latest'
|
||||
uses: ilammy/setup-nasm@v1.5.2
|
||||
uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2
|
||||
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.os == 'windows-latest'
|
||||
|
|
@ -44,12 +44,12 @@ jobs:
|
|||
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Rust (${{ matrix.version.name }})
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
with:
|
||||
toolchain: ${{ matrix.version.version }}
|
||||
|
||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@v2.56.13
|
||||
uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
|
||||
with:
|
||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||
|
||||
|
|
@ -71,19 +71,19 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Free Disk Space
|
||||
run: ./scripts/free-disk-space.sh
|
||||
|
||||
- name: Setup mold linker
|
||||
uses: rui314/setup-mold@v1
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
|
||||
- name: Install just, cargo-hack
|
||||
uses: taiki-e/install-action@v2.56.13
|
||||
uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
|
||||
with:
|
||||
tool: just,cargo-hack
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ on:
|
|||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -18,7 +18,7 @@ concurrency:
|
|||
jobs:
|
||||
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:
|
||||
needs: read_msrv
|
||||
|
|
@ -39,11 +39,11 @@ jobs:
|
|||
runs-on: ${{ matrix.target.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install nasm
|
||||
if: matrix.target.os == 'windows-latest'
|
||||
uses: ilammy/setup-nasm@v1.5.2
|
||||
uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2
|
||||
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.os == 'windows-latest'
|
||||
|
|
@ -56,15 +56,15 @@ jobs:
|
|||
|
||||
- name: Setup mold linker
|
||||
if: matrix.target.os == 'ubuntu-latest'
|
||||
uses: rui314/setup-mold@v1
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- name: Install Rust (${{ matrix.version.name }})
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
with:
|
||||
toolchain: ${{ matrix.version.version }}
|
||||
|
||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@v2.56.13
|
||||
uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
|
||||
with:
|
||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||
|
||||
|
|
@ -85,14 +85,18 @@ jobs:
|
|||
- name: 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:
|
||||
name: io-uring tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
with:
|
||||
toolchain: nightly
|
||||
|
||||
|
|
@ -105,15 +109,15 @@ jobs:
|
|||
name: doc tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
with:
|
||||
toolchain: nightly
|
||||
|
||||
- name: Install just
|
||||
uses: taiki-e/install-action@v2.56.13
|
||||
uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
|
||||
with:
|
||||
tool: just
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ name: Coverage
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -15,16 +15,16 @@ jobs:
|
|||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: llvm-tools
|
||||
|
||||
- name: Install just, cargo-llvm-cov, cargo-nextest
|
||||
uses: taiki-e/install-action@v2.56.13
|
||||
uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
|
||||
with:
|
||||
tool: just,cargo-llvm-cov,cargo-nextest
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
run: just test-coverage-codecov
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
files: codecov.json
|
||||
fail_ci_if_error: true
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ jobs:
|
|||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
|
|
@ -33,15 +33,15 @@ jobs:
|
|||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Check with Clippy
|
||||
uses: giraffate/clippy-action@v1.0.1
|
||||
uses: giraffate/clippy-action@13b9d32482f25d29ead141b79e7e04e7900281e0 # v1.0.1
|
||||
with:
|
||||
reporter: github-pr-check
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
@ -52,10 +52,10 @@ jobs:
|
|||
lint-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rust-docs
|
||||
|
|
@ -69,20 +69,20 @@ jobs:
|
|||
if: false # rustdoc mismatch currently
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
|
||||
with:
|
||||
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
|
||||
|
||||
- name: Install just
|
||||
uses: taiki-e/install-action@v2.56.13
|
||||
uses: taiki-e/install-action@0be4756f42223b67aa4b7df5effad59010cbf4b9 # v2.62.51
|
||||
with:
|
||||
tool: just
|
||||
|
||||
- name: Install cargo-check-external-types
|
||||
uses: taiki-e/cache-cargo-install-action@v2.2.0
|
||||
uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # v2.3.1
|
||||
with:
|
||||
tool: cargo-check-external-types
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
## 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.
|
||||
|
||||
## 0.6.6
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.6"
|
||||
version = "0.6.8"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
||||
description = "Static file serving for Actix Web"
|
||||
keywords = ["actix", "http", "async", "futures"]
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.6)
|
||||
[](https://docs.rs/actix-files/0.6.8)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.6)
|
||||
[](https://deps.rs/crate/actix-files/0.6.8)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ use pin_project_lite::pin_project;
|
|||
|
||||
use super::named::File;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum ReadMode {
|
||||
Sync,
|
||||
Async,
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
/// Adapter to read a `std::file::File` in chunks.
|
||||
#[doc(hidden)]
|
||||
|
|
@ -24,6 +30,7 @@ pin_project! {
|
|||
state: ChunkedReadFileState<Fut>,
|
||||
counter: u64,
|
||||
callback: F,
|
||||
read_mode: ReadMode,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +64,7 @@ pub(crate) fn new_chunked_read(
|
|||
size: u64,
|
||||
offset: u64,
|
||||
file: File,
|
||||
read_mode_threshold: u64,
|
||||
) -> impl Stream<Item = Result<Bytes, Error>> {
|
||||
ChunkedReadFile {
|
||||
size,
|
||||
|
|
@ -69,18 +77,22 @@ pub(crate) fn new_chunked_read(
|
|||
},
|
||||
counter: 0,
|
||||
callback: chunked_read_file_callback,
|
||||
read_mode: if size < read_mode_threshold {
|
||||
ReadMode::Sync
|
||||
} else {
|
||||
ReadMode::Async
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
async fn chunked_read_file_callback(
|
||||
fn chunked_read_file_callback_sync(
|
||||
mut file: File,
|
||||
offset: u64,
|
||||
max_bytes: usize,
|
||||
) -> Result<(File, Bytes), Error> {
|
||||
) -> Result<(File, Bytes), io::Error> {
|
||||
use io::{Read as _, Seek as _};
|
||||
|
||||
let res = actix_web::web::block(move || {
|
||||
let mut buf = Vec::with_capacity(max_bytes);
|
||||
|
||||
file.seek(io::SeekFrom::Start(offset))?;
|
||||
|
|
@ -92,8 +104,23 @@ async fn chunked_read_file_callback(
|
|||
} else {
|
||||
Ok((file, Bytes::from(buf)))
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
}
|
||||
|
||||
#[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??
|
||||
}
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
|
@ -171,7 +198,7 @@ where
|
|||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||
where
|
||||
F: Fn(File, u64, usize) -> Fut,
|
||||
F: Fn(File, u64, usize, ReadMode) -> Fut,
|
||||
Fut: Future<Output = Result<(File, Bytes), Error>>,
|
||||
{
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
|
@ -193,7 +220,7 @@ where
|
|||
.take()
|
||||
.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
|
||||
.project_replace(ChunkedReadFileState::Future { fut });
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ pub struct Files {
|
|||
index: Option<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
with_permanent_redirect: bool,
|
||||
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
||||
renderer: Rc<DirectoryRenderer>,
|
||||
mime_override: Option<Rc<MimeOverride>>,
|
||||
|
|
@ -49,6 +50,7 @@ pub struct Files {
|
|||
use_guards: Option<Rc<dyn Guard>>,
|
||||
guards: Vec<Rc<dyn Guard>>,
|
||||
hidden_files: bool,
|
||||
read_mode_threshold: u64,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Files {
|
||||
|
|
@ -64,6 +66,7 @@ impl Clone for Files {
|
|||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
with_permanent_redirect: self.with_permanent_redirect,
|
||||
default: self.default.clone(),
|
||||
renderer: self.renderer.clone(),
|
||||
file_flags: self.file_flags,
|
||||
|
|
@ -73,6 +76,7 @@ impl Clone for Files {
|
|||
use_guards: self.use_guards.clone(),
|
||||
guards: self.guards.clone(),
|
||||
hidden_files: self.hidden_files,
|
||||
read_mode_threshold: self.read_mode_threshold,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,6 +115,7 @@ impl Files {
|
|||
index: None,
|
||||
show_index: false,
|
||||
redirect_to_slash: false,
|
||||
with_permanent_redirect: false,
|
||||
default: Rc::new(RefCell::new(None)),
|
||||
renderer: Rc::new(directory_listing),
|
||||
mime_override: None,
|
||||
|
|
@ -119,6 +124,7 @@ impl Files {
|
|||
use_guards: None,
|
||||
guards: Vec::new(),
|
||||
hidden_files: false,
|
||||
read_mode_threshold: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +147,14 @@ impl Files {
|
|||
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.
|
||||
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
|
||||
where
|
||||
|
|
@ -204,6 +218,23 @@ impl Files {
|
|||
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.
|
||||
///
|
||||
/// Default is true.
|
||||
|
|
@ -367,6 +398,8 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||
file_flags: self.file_flags,
|
||||
guards: self.use_guards.clone(),
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -736,7 +736,21 @@ mod tests {
|
|||
.await;
|
||||
let req = TestRequest::with_uri("/tests").to_request();
|
||||
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
|
||||
let srv = test::init_service(
|
||||
|
|
@ -749,7 +763,7 @@ mod tests {
|
|||
.await;
|
||||
let req = TestRequest::with_uri("/tests").to_request();
|
||||
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
|
||||
let req = TestRequest::with_uri("/not_existing").to_request();
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ pub struct NamedFile {
|
|||
pub(crate) content_type: Mime,
|
||||
pub(crate) content_disposition: ContentDisposition,
|
||||
pub(crate) encoding: Option<ContentEncoding>,
|
||||
pub(crate) read_mode_threshold: u64,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
|
|
@ -200,6 +201,7 @@ impl NamedFile {
|
|||
encoding,
|
||||
status_code: StatusCode::OK,
|
||||
flags: Flags::default(),
|
||||
read_mode_threshold: 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -353,6 +355,23 @@ impl NamedFile {
|
|||
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.
|
||||
///
|
||||
/// Default is true.
|
||||
|
|
@ -440,7 +459,8 @@ impl NamedFile {
|
|||
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);
|
||||
}
|
||||
|
|
@ -577,7 +597,7 @@ impl NamedFile {
|
|||
.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() {
|
||||
res.status(StatusCode::PARTIAL_CONTENT);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ pub struct FilesServiceInner {
|
|||
pub(crate) file_flags: named::Flags,
|
||||
pub(crate) guards: Option<Rc<dyn Guard>>,
|
||||
pub(crate) hidden_files: bool,
|
||||
pub(crate) size_threshold: u64,
|
||||
pub(crate) with_permanent_redirect: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for FilesServiceInner {
|
||||
|
|
@ -70,7 +72,9 @@ impl FilesService {
|
|||
named_file.flags = self.file_flags;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -145,11 +149,15 @@ impl Service<ServiceRequest> for FilesService {
|
|||
{
|
||||
let redirect_to = format!("{}/", req.path());
|
||||
|
||||
return Ok(req.into_response(
|
||||
HttpResponse::Found()
|
||||
let response = if this.with_permanent_redirect {
|
||||
HttpResponse::PermanentRedirect()
|
||||
} else {
|
||||
HttpResponse::TemporaryRedirect()
|
||||
}
|
||||
.insert_header((header::LOCATION, redirect_to))
|
||||
.finish(),
|
||||
));
|
||||
.finish();
|
||||
|
||||
return Ok(req.into_response(response));
|
||||
}
|
||||
|
||||
match this.index {
|
||||
|
|
@ -169,17 +177,7 @@ impl Service<ServiceRequest> for FilesService {
|
|||
}
|
||||
} else {
|
||||
match NamedFile::open_async(&path).await {
|
||||
Ok(mut 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))
|
||||
}
|
||||
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
||||
Err(err) => this.handle_err(err, req).await,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
extern crate tls_openssl as openssl;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@
|
|||
|
||||
## 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`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.11.0"
|
||||
version = "3.11.2"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
||||
description = "HTTP types and services for the Actix ecosystem"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
|
|
@ -17,7 +17,6 @@ edition.workspace = true
|
|||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
features = [
|
||||
"http2",
|
||||
"ws",
|
||||
|
|
@ -119,7 +118,7 @@ tokio-util = { version = "0.7", features = ["io", "codec"] }
|
|||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
# http2
|
||||
h2 = { version = "0.3.26", optional = true }
|
||||
h2 = { version = "0.3.27", optional = true }
|
||||
|
||||
# websockets
|
||||
base64 = { version = "0.22", optional = true }
|
||||
|
|
@ -157,7 +156,7 @@ serde_json = "1.0"
|
|||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.55" }
|
||||
tls-rustls_023 = { package = "rustls", version = "0.23" }
|
||||
tokio = { version = "1.38.2", features = ["net", "rt", "macros"] }
|
||||
tokio = { version = "1.38.2", features = ["net", "rt", "macros", "sync"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.11.0)
|
||||
[](https://docs.rs/actix-http/3.11.2)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.11.0)
|
||||
[](https://deps.rs/crate/actix-http/3.11.2)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
body::{BoxBody, MessageBody},
|
||||
h1::{self, ExpectHandler, H1Service, UpgradeHandler},
|
||||
service::HttpService,
|
||||
ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfig,
|
||||
ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfigBuilder,
|
||||
};
|
||||
|
||||
/// An HTTP service builder.
|
||||
|
|
@ -19,6 +19,7 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
|||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
h1_allow_half_closed: bool,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
|
|
@ -40,6 +41,7 @@ where
|
|||
client_disconnect_timeout: Duration::ZERO,
|
||||
secure: false,
|
||||
local_addr: None,
|
||||
h1_allow_half_closed: true,
|
||||
|
||||
// dispatcher parts
|
||||
expect: ExpectHandler,
|
||||
|
|
@ -124,6 +126,18 @@ where
|
|||
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.
|
||||
///
|
||||
/// Service get called with request that contains `EXPECT` header.
|
||||
|
|
@ -142,6 +156,7 @@ where
|
|||
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||
secure: self.secure,
|
||||
local_addr: self.local_addr,
|
||||
h1_allow_half_closed: self.h1_allow_half_closed,
|
||||
expect: expect.into_factory(),
|
||||
upgrade: self.upgrade,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
|
|
@ -166,6 +181,7 @@ where
|
|||
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||
secure: self.secure,
|
||||
local_addr: self.local_addr,
|
||||
h1_allow_half_closed: self.h1_allow_half_closed,
|
||||
expect: self.expect,
|
||||
upgrade: Some(upgrade.into_factory()),
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
|
|
@ -195,13 +211,14 @@ where
|
|||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
let cfg = ServiceConfigBuilder::new()
|
||||
.keep_alive(self.keep_alive)
|
||||
.client_request_timeout(self.client_request_timeout)
|
||||
.client_disconnect_timeout(self.client_disconnect_timeout)
|
||||
.secure(self.secure)
|
||||
.local_addr(self.local_addr)
|
||||
.h1_allow_half_closed(self.h1_allow_half_closed)
|
||||
.build();
|
||||
|
||||
H1Service::with_config(cfg, service.into_factory())
|
||||
.expect(self.expect)
|
||||
|
|
@ -220,13 +237,14 @@ where
|
|||
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
let cfg = ServiceConfigBuilder::new()
|
||||
.keep_alive(self.keep_alive)
|
||||
.client_request_timeout(self.client_request_timeout)
|
||||
.client_disconnect_timeout(self.client_disconnect_timeout)
|
||||
.secure(self.secure)
|
||||
.local_addr(self.local_addr)
|
||||
.h1_allow_half_closed(self.h1_allow_half_closed)
|
||||
.build();
|
||||
|
||||
crate::h2::H2Service::with_config(cfg, service.into_factory())
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
|
|
@ -242,13 +260,14 @@ where
|
|||
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
let cfg = ServiceConfigBuilder::new()
|
||||
.keep_alive(self.keep_alive)
|
||||
.client_request_timeout(self.client_request_timeout)
|
||||
.client_disconnect_timeout(self.client_disconnect_timeout)
|
||||
.secure(self.secure)
|
||||
.local_addr(self.local_addr)
|
||||
.h1_allow_half_closed(self.h1_allow_half_closed)
|
||||
.build();
|
||||
|
||||
HttpService::with_config(cfg, service.into_factory())
|
||||
.expect(self.expect)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::{
|
||||
net,
|
||||
net::SocketAddr,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
|
@ -8,8 +8,76 @@ use bytes::BytesMut;
|
|||
|
||||
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.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ServiceConfig(Rc<Inner>);
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -18,19 +86,22 @@ struct Inner {
|
|||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<std::net::SocketAddr>,
|
||||
local_addr: Option<SocketAddr>,
|
||||
date_service: DateService,
|
||||
h1_allow_half_closed: bool,
|
||||
}
|
||||
|
||||
impl Default for ServiceConfig {
|
||||
impl Default for Inner {
|
||||
fn default() -> Self {
|
||||
Self::new(
|
||||
KeepAlive::default(),
|
||||
Duration::from_secs(5),
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
Self {
|
||||
keep_alive: KeepAlive::default(),
|
||||
client_request_timeout: Duration::from_secs(5),
|
||||
client_disconnect_timeout: Duration::ZERO,
|
||||
secure: false,
|
||||
local_addr: None,
|
||||
date_service: DateService::new(),
|
||||
h1_allow_half_closed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +112,7 @@ impl ServiceConfig {
|
|||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
local_addr: Option<SocketAddr>,
|
||||
) -> ServiceConfig {
|
||||
ServiceConfig(Rc::new(Inner {
|
||||
keep_alive: keep_alive.normalize(),
|
||||
|
|
@ -50,6 +121,7 @@ impl ServiceConfig {
|
|||
secure,
|
||||
local_addr,
|
||||
date_service: DateService::new(),
|
||||
h1_allow_half_closed: true,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +135,7 @@ impl ServiceConfig {
|
|||
///
|
||||
/// Returns `None` for connections via UDS (Unix Domain Socket).
|
||||
#[inline]
|
||||
pub fn local_addr(&self) -> Option<net::SocketAddr> {
|
||||
pub fn local_addr(&self) -> Option<SocketAddr> {
|
||||
self.0.local_addr
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +172,15 @@ impl ServiceConfig {
|
|||
(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 {
|
||||
self.0.date_service.now()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -386,7 +386,14 @@ where
|
|||
let mut this = self.project();
|
||||
this.state.set(match size {
|
||||
BodySize::None | BodySize::Sized(0) => {
|
||||
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::SendPayload { body },
|
||||
|
|
@ -404,7 +411,14 @@ where
|
|||
let mut this = self.project();
|
||||
this.state.set(match size {
|
||||
BodySize::None | BodySize::Sized(0) => {
|
||||
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::SendErrorPayload { body },
|
||||
|
|
@ -503,10 +517,22 @@ where
|
|||
Poll::Ready(None) => {
|
||||
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.
|
||||
// set state to None and handle next message
|
||||
this.state.set(State::None);
|
||||
|
||||
if not_pipelined && payload_unfinished {
|
||||
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
|
||||
} else {
|
||||
this.flags.insert(Flags::FINISHED);
|
||||
}
|
||||
|
||||
continue 'res;
|
||||
}
|
||||
|
|
@ -542,10 +568,22 @@ where
|
|||
Poll::Ready(None) => {
|
||||
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
|
||||
this.state.set(State::None);
|
||||
|
||||
if not_pipelined && payload_unfinished {
|
||||
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
|
||||
} else {
|
||||
this.flags.insert(Flags::FINISHED);
|
||||
}
|
||||
|
||||
continue 'res;
|
||||
}
|
||||
|
|
@ -1181,8 +1219,16 @@ where
|
|||
let inner_p = inner.as_mut().project();
|
||||
let state_is_none = inner_p.state.is_none();
|
||||
|
||||
// read half is closed; we do not process any responses
|
||||
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none {
|
||||
// If the read-half is closed, we start the shutdown procedure if either is
|
||||
// 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");
|
||||
inner_p.flags.insert(Flags::SHUTDOWN);
|
||||
}
|
||||
|
|
@ -1216,6 +1262,9 @@ where
|
|||
inner_p.shutdown_timer,
|
||||
);
|
||||
|
||||
if inner_p.flags.contains(Flags::SHUTDOWN) {
|
||||
cx.waker().wake_by_ref();
|
||||
}
|
||||
Poll::Pending
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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_rt::{pin, time::sleep};
|
||||
|
|
@ -9,7 +15,7 @@ use futures_util::future::lazy;
|
|||
|
||||
use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags};
|
||||
use crate::{
|
||||
body::MessageBody,
|
||||
body::{BoxBody, MessageBody},
|
||||
config::ServiceConfig,
|
||||
h1::{Codec, ExpectHandler, UpgradeHandler},
|
||||
service::HttpFlow,
|
||||
|
|
@ -17,6 +23,26 @@ use crate::{
|
|||
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> {
|
||||
memchr::memmem::find(&haystack[from..], needle)
|
||||
}
|
||||
|
|
@ -509,6 +535,73 @@ async fn pipelining_ok_then_ok() {
|
|||
.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]
|
||||
async fn pipelining_ok_then_bad() {
|
||||
lazy(|cx| {
|
||||
|
|
@ -924,6 +1017,91 @@ async fn handler_drop_payload() {
|
|||
.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 {
|
||||
let mut msg = msg
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -200,11 +200,13 @@ impl Inner {
|
|||
#[inline]
|
||||
fn set_error(&mut self, err: PayloadError) {
|
||||
self.err = Some(err);
|
||||
self.wake();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn feed_eof(&mut self) {
|
||||
self.eof = true;
|
||||
self.wake();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -253,8 +255,13 @@ impl Inner {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{task::Poll, time::Duration};
|
||||
|
||||
use actix_rt::time::timeout;
|
||||
use actix_utils::future::poll_fn;
|
||||
use futures_util::{FutureExt, StreamExt};
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -263,6 +270,67 @@ mod tests {
|
|||
|
||||
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]
|
||||
async fn test_unread_data() {
|
||||
let (_, mut payload) = Payload::create(false);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
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::{
|
||||
builder::HttpServiceBuilder,
|
||||
config::ServiceConfig,
|
||||
config::{ServiceConfig, ServiceConfigBuilder},
|
||||
error::Error,
|
||||
extensions::Extensions,
|
||||
header::ContentEncoding,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use std::{
|
|||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use http::{Method, Uri, Version};
|
||||
use http::{header, Method, Uri, Version};
|
||||
|
||||
use crate::{
|
||||
header::{HeaderMap, TryIntoHeaderPair},
|
||||
|
|
@ -98,9 +98,13 @@ impl TestRequest {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
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());
|
||||
self
|
||||
}
|
||||
|
|
@ -271,6 +275,7 @@ impl TestSeqBuffer {
|
|||
{
|
||||
Self(Rc::new(RefCell::new(TestSeqInner {
|
||||
read_buf: data.into(),
|
||||
read_closed: false,
|
||||
write_buf: BytesMut::new(),
|
||||
err: None,
|
||||
})))
|
||||
|
|
@ -289,36 +294,59 @@ impl TestSeqBuffer {
|
|||
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>> {
|
||||
Ref::map(self.0.borrow(), |inner| &inner.err)
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.read_buf
|
||||
.extend_from_slice(data.as_ref())
|
||||
let mut inner = self.0.borrow_mut();
|
||||
if inner.read_closed {
|
||||
panic!("Tried to extend the read buffer after calling close_read");
|
||||
}
|
||||
|
||||
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 {
|
||||
read_buf: BytesMut,
|
||||
read_closed: bool,
|
||||
write_buf: BytesMut,
|
||||
err: Option<io::Error>,
|
||||
}
|
||||
|
||||
impl io::Read for TestSeqBuffer {
|
||||
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
|
||||
if self.0.borrow().read_buf.is_empty() {
|
||||
if self.0.borrow().err.is_some() {
|
||||
Err(self.0.borrow_mut().err.take().unwrap())
|
||||
let mut inner = self.0.borrow_mut();
|
||||
|
||||
if inner.read_buf.is_empty() {
|
||||
if let Some(err) = inner.err.take() {
|
||||
Err(err)
|
||||
} else if inner.read_closed {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
|
||||
}
|
||||
} else {
|
||||
let size = std::cmp::min(self.0.borrow().read_buf.len(), dst.len());
|
||||
let b = self.0.borrow_mut().read_buf.split_to(size);
|
||||
let size = std::cmp::min(inner.read_buf.len(), dst.len());
|
||||
let b = inner.read_buf.split_to(size);
|
||||
dst[..size].copy_from_slice(&b);
|
||||
Ok(size)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,11 +94,21 @@ impl Parser {
|
|||
Some(res) => res,
|
||||
};
|
||||
|
||||
let frame_len = match idx.checked_add(length) {
|
||||
Some(len) => len,
|
||||
None => return Err(ProtocolError::Overflow),
|
||||
};
|
||||
|
||||
// not enough data
|
||||
if src.len() < idx + length {
|
||||
if src.len() < frame_len {
|
||||
let min_length = min(length, max_size);
|
||||
if src.capacity() < idx + min_length {
|
||||
src.reserve(idx + min_length - src.capacity());
|
||||
let required_cap = match idx.checked_add(min_length) {
|
||||
Some(cap) => cap,
|
||||
None => return Err(ProtocolError::Overflow),
|
||||
};
|
||||
|
||||
if src.capacity() < required_cap {
|
||||
src.reserve(required_cap - src.capacity());
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
|
@ -402,4 +412,14 @@ mod tests {
|
|||
Parser::write_close(&mut buf, None, false);
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ edition.workspace = true
|
|||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
all-features = true
|
||||
|
||||
[lib]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![allow(clippy::disallowed_names)] // false positives in some macro expansions
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ license.workspace = true
|
|||
edition.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
all-features = true
|
||||
|
||||
[package.metadata.cargo_check_external_types]
|
||||
|
|
|
|||
|
|
@ -24,9 +24,10 @@ Due to additional requirements for `multipart/form-data` requests, the higher le
|
|||
## Examples
|
||||
|
||||
```rust
|
||||
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 actix_web::{middleware::Logger, post, App, HttpServer, Responder};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -36,22 +37,34 @@ struct Metadata {
|
|||
|
||||
#[derive(Debug, MultipartForm)]
|
||||
struct UploadForm {
|
||||
// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
|
||||
#[multipart(limit = "100MB")]
|
||||
file: TempFile,
|
||||
json: MpJson<Metadata>,
|
||||
}
|
||||
|
||||
#[post("/videos")]
|
||||
pub async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
|
||||
async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
|
||||
format!(
|
||||
"Uploaded file {}, with size: {}",
|
||||
form.json.name, form.file.size
|
||||
"Uploaded file {}, with size: {}\ntemporary file ({}) was deleted\n",
|
||||
form.json.name,
|
||||
form.file.size,
|
||||
form.file.file.path().display(),
|
||||
)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
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"));
|
||||
|
||||
HttpServer::new(move || {
|
||||
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 -->
|
||||
|
||||
[More available in the examples repo →](https://github.com/actix/examples/tree/master/forms/multipart)
|
||||
[More available in the examples repo →](https://github.com/actix/examples/tree/main/forms/multipart)
|
||||
|
|
|
|||
|
|
@ -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 serde::Deserialize;
|
||||
|
||||
|
|
@ -9,6 +11,7 @@ struct Metadata {
|
|||
|
||||
#[derive(Debug, MultipartForm)]
|
||||
struct UploadForm {
|
||||
// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
|
||||
#[multipart(limit = "100MB")]
|
||||
file: TempFile,
|
||||
json: MpJson<Metadata>,
|
||||
|
|
@ -28,7 +31,13 @@ async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Resp
|
|||
async fn main() -> std::io::Result<()> {
|
||||
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 || {
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
//! ```no_run
|
||||
//! 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;
|
||||
//!
|
||||
//! #[derive(Debug, Deserialize)]
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
//!
|
||||
//! #[derive(Debug, MultipartForm)]
|
||||
//! struct UploadForm {
|
||||
//! // Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
|
||||
//! #[multipart(limit = "100MB")]
|
||||
//! file: TempFile,
|
||||
//! json: MpJson<Metadata>,
|
||||
|
|
@ -38,7 +39,12 @@
|
|||
//!
|
||||
//! #[actix_web::main]
|
||||
//! async fn main() -> std::io::Result<()> {
|
||||
//! HttpServer::new(move || App::new().service(post_video))
|
||||
//! HttpServer::new(move || {
|
||||
//! App::new()
|
||||
//! .service(post_video)
|
||||
//! // 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_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
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ macro_rules! register {
|
|||
register!(finish => "(.*)", "(.*)", "(.*)", "(.*)")
|
||||
}};
|
||||
(finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{
|
||||
#[expect(clippy::useless_concat)]
|
||||
let arr = [
|
||||
concat!("/authorizations"),
|
||||
concat!("/authorizations/", $p1),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
mod de;
|
||||
mod path;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
extern crate tls_openssl as openssl;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ allowed_external_types = [
|
|||
actix = { version = ">=0.12, <0.14", default-features = false }
|
||||
actix-codec = "0.5"
|
||||
actix-http = "3"
|
||||
actix-web = { version = "4", default-features = false }
|
||||
actix-web = { version = "4", default-features = false, features = ["ws"] }
|
||||
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
mod context;
|
||||
pub mod ws;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
#![recursion_limit = "512"]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@
|
|||
|
||||
- Add `experimental-introspection` feature for retrieving configured route paths and HTTP methods.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.11.0"
|
||||
version = "4.12.0"
|
||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
|
|
@ -17,7 +17,6 @@ edition.workspace = true
|
|||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
features = [
|
||||
"macros",
|
||||
"openssl",
|
||||
|
|
@ -53,7 +52,6 @@ allowed_external_types = [
|
|||
"serde_json::*",
|
||||
"serde_urlencoded::*",
|
||||
"serde::*",
|
||||
"serde::*",
|
||||
"tokio::*",
|
||||
"url::*",
|
||||
]
|
||||
|
|
@ -68,6 +66,7 @@ default = [
|
|||
"http2",
|
||||
"unicode",
|
||||
"compat",
|
||||
"ws",
|
||||
]
|
||||
|
||||
# Brotli algorithm content-encoding support
|
||||
|
|
@ -86,9 +85,12 @@ cookies = ["dep:cookie"]
|
|||
# Secure & signed cookies
|
||||
secure-cookies = ["cookies", "cookie/secure"]
|
||||
|
||||
# HTTP/2 support (including h2c).
|
||||
# HTTP/2 support (including h2c)
|
||||
http2 = ["actix-http/http2"]
|
||||
|
||||
# WebSocket support
|
||||
ws = ["actix-http/ws"]
|
||||
|
||||
# TLS via OpenSSL
|
||||
openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
|
||||
|
||||
|
|
@ -135,7 +137,7 @@ actix-service = "2"
|
|||
actix-tls = { version = "3.4", default-features = false, optional = true }
|
||||
actix-utils = "3"
|
||||
|
||||
actix-http = { version = "3.11", features = ["ws"] }
|
||||
actix-http = "3.11"
|
||||
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
|
||||
actix-web-codegen = { version = "4.3", optional = true, default-features = false }
|
||||
|
||||
|
|
|
|||
|
|
@ -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>>()`.
|
||||
|
||||
- Cookie handling has been offloaded to the `cookie` crate:
|
||||
|
||||
- `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
||||
- Some types now require lifetime parameters.
|
||||
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ An alternative [path param type with public field but no `Deref` impl is availab
|
|||
|
||||
## 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
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.11.0)
|
||||
[](https://docs.rs/actix-web/4.12.0)
|
||||

|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.11.0)
|
||||
[](https://deps.rs/crate/actix-web/4.12.0)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
- [Website & User Guide](https://actix.rs)
|
||||
- [Examples Repository](https://github.com/actix/examples)
|
||||
- [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
|
||||
|
||||
|
|
@ -78,23 +78,23 @@ async fn main() -> std::io::Result<()> {
|
|||
|
||||
### More Examples
|
||||
|
||||
- [Hello World](https://github.com/actix/examples/tree/master/basics/hello-world)
|
||||
- [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics)
|
||||
- [Application State](https://github.com/actix/examples/tree/master/basics/state)
|
||||
- [JSON Handling](https://github.com/actix/examples/tree/master/json/json)
|
||||
- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart)
|
||||
- [MongoDB Integration](https://github.com/actix/examples/tree/master/databases/mongodb)
|
||||
- [Diesel Integration](https://github.com/actix/examples/tree/master/databases/diesel)
|
||||
- [SQLite Integration](https://github.com/actix/examples/tree/master/databases/sqlite)
|
||||
- [Postgres Integration](https://github.com/actix/examples/tree/master/databases/postgres)
|
||||
- [Tera Templates](https://github.com/actix/examples/tree/master/templating/tera)
|
||||
- [Askama Templates](https://github.com/actix/examples/tree/master/templating/askama)
|
||||
- [HTTPS using Rustls](https://github.com/actix/examples/tree/master/https-tls/rustls)
|
||||
- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/https-tls/openssl)
|
||||
- [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets)
|
||||
- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat)
|
||||
- [Hello World](https://github.com/actix/examples/tree/mainasics/hello-world)
|
||||
- [Basic Setup](https://github.com/actix/examples/tree/mainasics/basics)
|
||||
- [Application State](https://github.com/actix/examples/tree/mainasics/state)
|
||||
- [JSON Handling](https://github.com/actix/examples/tree/mainson/json)
|
||||
- [Multipart Streams](https://github.com/actix/examples/tree/mainorms/multipart)
|
||||
- [MongoDB Integration](https://github.com/actix/examples/tree/mainatabases/mongodb)
|
||||
- [Diesel Integration](https://github.com/actix/examples/tree/mainatabases/diesel)
|
||||
- [SQLite Integration](https://github.com/actix/examples/tree/mainatabases/sqlite)
|
||||
- [Postgres Integration](https://github.com/actix/examples/tree/mainatabases/postgres)
|
||||
- [Tera Templates](https://github.com/actix/examples/tree/mainemplating/tera)
|
||||
- [Askama Templates](https://github.com/actix/examples/tree/mainemplating/askama)
|
||||
- [HTTPS using Rustls](https://github.com/actix/examples/tree/mainttps-tls/rustls)
|
||||
- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/mainttps-tls/openssl)
|
||||
- [Simple WebSocket](https://github.com/actix/examples/tree/mainebsockets)
|
||||
- [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
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//! properties and pass them to a handler through request-local data.
|
||||
//!
|
||||
//! 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};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ mod response_error;
|
|||
|
||||
pub(crate) use self::macros::{downcast_dyn, downcast_get_type_id};
|
||||
pub use self::{error::Error, internal::*, response_error::ResponseError};
|
||||
pub use crate::types::EitherExtractError;
|
||||
|
||||
/// A convenience [`Result`](std::result::Result) for Actix Web operations.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ use std::{
|
|||
io::{self, Write as _},
|
||||
};
|
||||
|
||||
use actix_http::Response;
|
||||
use bytes::BytesMut;
|
||||
|
||||
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 {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ws")]
|
||||
impl ResponseError for actix_http::ws::HandshakeError {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
pub use actix_http::{body, HttpMessage};
|
||||
#[cfg(feature = "cookies")]
|
||||
|
|
|
|||
|
|
@ -2,16 +2,79 @@
|
|||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
- `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
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use futures_core::Stream;
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
body::{BodyStream, BoxBody, MessageBody},
|
||||
body::{BodyStream, BoxBody, MessageBody, SizedStream},
|
||||
dev::Extensions,
|
||||
error::{Error, JsonPayloadError},
|
||||
http::{
|
||||
|
|
@ -318,14 +318,36 @@ impl HttpResponseBuilder {
|
|||
/// Set a streaming body and build the `HttpResponse`.
|
||||
///
|
||||
/// `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]
|
||||
pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
// 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`.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ struct Config {
|
|||
keep_alive: KeepAlive,
|
||||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
h1_allow_half_closed: bool,
|
||||
#[allow(dead_code)] // only dead when no TLS features are enabled
|
||||
tls_handshake_timeout: Option<Duration>,
|
||||
}
|
||||
|
|
@ -116,6 +117,7 @@ where
|
|||
keep_alive: KeepAlive::default(),
|
||||
client_request_timeout: Duration::from_secs(5),
|
||||
client_disconnect_timeout: Duration::from_secs(1),
|
||||
h1_allow_half_closed: true,
|
||||
tls_handshake_timeout: None,
|
||||
})),
|
||||
backlog: 1024,
|
||||
|
|
@ -257,6 +259,18 @@ where
|
|||
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.
|
||||
///
|
||||
/// It will receive a `&std::any::Any`, which contains underlying connection type and an
|
||||
|
|
@ -558,6 +572,7 @@ where
|
|||
.keep_alive(cfg.keep_alive)
|
||||
.client_request_timeout(cfg.client_request_timeout)
|
||||
.client_disconnect_timeout(cfg.client_disconnect_timeout)
|
||||
.h1_allow_half_closed(cfg.h1_allow_half_closed)
|
||||
.local_addr(addr);
|
||||
|
||||
if let Some(handler) = on_connect_fn.clone() {
|
||||
|
|
@ -602,6 +617,7 @@ where
|
|||
.keep_alive(cfg.keep_alive)
|
||||
.client_request_timeout(cfg.client_request_timeout)
|
||||
.client_disconnect_timeout(cfg.client_disconnect_timeout)
|
||||
.h1_allow_half_closed(cfg.h1_allow_half_closed)
|
||||
.local_addr(addr);
|
||||
|
||||
if let Some(handler) = on_connect_fn.clone() {
|
||||
|
|
@ -677,6 +693,7 @@ where
|
|||
let svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
.client_request_timeout(c.client_request_timeout)
|
||||
.h1_allow_half_closed(c.h1_allow_half_closed)
|
||||
.client_disconnect_timeout(c.client_disconnect_timeout);
|
||||
|
||||
let svc = if let Some(handler) = on_connect_fn.clone() {
|
||||
|
|
@ -728,6 +745,7 @@ where
|
|||
let svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
.client_request_timeout(c.client_request_timeout)
|
||||
.h1_allow_half_closed(c.h1_allow_half_closed)
|
||||
.client_disconnect_timeout(c.client_disconnect_timeout);
|
||||
|
||||
let svc = if let Some(handler) = on_connect_fn.clone() {
|
||||
|
|
@ -794,6 +812,7 @@ where
|
|||
let svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
.client_request_timeout(c.client_request_timeout)
|
||||
.h1_allow_half_closed(c.h1_allow_half_closed)
|
||||
.client_disconnect_timeout(c.client_disconnect_timeout);
|
||||
|
||||
let svc = if let Some(handler) = on_connect_fn.clone() {
|
||||
|
|
@ -860,6 +879,7 @@ where
|
|||
let svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
.client_request_timeout(c.client_request_timeout)
|
||||
.h1_allow_half_closed(c.h1_allow_half_closed)
|
||||
.client_disconnect_timeout(c.client_disconnect_timeout);
|
||||
|
||||
let svc = if let Some(handler) = on_connect_fn.clone() {
|
||||
|
|
@ -927,6 +947,7 @@ where
|
|||
.keep_alive(c.keep_alive)
|
||||
.client_request_timeout(c.client_request_timeout)
|
||||
.client_disconnect_timeout(c.client_disconnect_timeout)
|
||||
.h1_allow_half_closed(c.h1_allow_half_closed)
|
||||
.local_addr(addr);
|
||||
|
||||
let svc = if let Some(handler) = on_connect_fn.clone() {
|
||||
|
|
@ -995,6 +1016,7 @@ where
|
|||
.keep_alive(c.keep_alive)
|
||||
.client_request_timeout(c.client_request_timeout)
|
||||
.client_disconnect_timeout(c.client_disconnect_timeout)
|
||||
.h1_allow_half_closed(c.h1_allow_half_closed)
|
||||
.finish(map_config(fac, move |_| config.clone())),
|
||||
)
|
||||
},
|
||||
|
|
@ -1036,6 +1058,7 @@ where
|
|||
let mut svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
.client_request_timeout(c.client_request_timeout)
|
||||
.h1_allow_half_closed(c.h1_allow_half_closed)
|
||||
.client_disconnect_timeout(c.client_disconnect_timeout);
|
||||
|
||||
if let Some(handler) = on_connect_fn.clone() {
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@ mod tests {
|
|||
}
|
||||
));
|
||||
|
||||
let (req, mut pl) = TestRequest::default()
|
||||
let (mut req, mut pl) = TestRequest::default()
|
||||
.insert_header((
|
||||
header::CONTENT_TYPE,
|
||||
header::HeaderValue::from_static("application/json"),
|
||||
|
|
@ -624,6 +624,7 @@ mod tests {
|
|||
.set_payload(Bytes::from_static(&[0u8; 1000]))
|
||||
.to_http_parts();
|
||||
|
||||
req.head_mut().headers_mut().remove(header::CONTENT_LENGTH);
|
||||
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true)
|
||||
.limit(100)
|
||||
.await;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ mod query;
|
|||
mod readlines;
|
||||
|
||||
pub use self::{
|
||||
either::Either,
|
||||
either::{Either, EitherExtractError},
|
||||
form::{Form, FormConfig, UrlEncoded},
|
||||
header::Header,
|
||||
html::Html,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ use crate::{
|
|||
///
|
||||
/// 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
|
||||
/// 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 `[^{}/]+`.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
## 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`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "awc"
|
||||
version = "3.7.0"
|
||||
version = "3.8.1"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Async HTTP and WebSocket client library"
|
||||
keywords = ["actix", "http", "framework", "async", "web"]
|
||||
|
|
@ -16,7 +16,6 @@ license = "MIT OR Apache-2.0"
|
|||
edition = "2021"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
features = [
|
||||
"cookies",
|
||||
"openssl",
|
||||
|
|
@ -83,8 +82,10 @@ compress-zstd = ["actix-http/compress-zstd", "__compress"]
|
|||
# Cookie parsing and cookie jar
|
||||
cookies = ["dep:cookie"]
|
||||
|
||||
# Use `trust-dns-resolver` crate as DNS resolver
|
||||
trust-dns = ["trust-dns-resolver"]
|
||||
# Use `hickory-dns-resolver` crate as 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.
|
||||
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||
|
|
@ -109,7 +110,7 @@ cfg-if = "1"
|
|||
derive_more = { version = "2", features = ["display", "error", "from"] }
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
|
||||
h2 = "0.3.26"
|
||||
h2 = "0.3.27"
|
||||
http = "0.2.7"
|
||||
itoa = "1"
|
||||
log = "0.4"
|
||||
|
|
@ -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_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]
|
||||
actix-http = { version = "3.7", features = ["openssl"] }
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/awc)
|
||||
[](https://docs.rs/awc/3.7.0)
|
||||
[](https://docs.rs/awc/3.8.1)
|
||||

|
||||
[](https://deps.rs/crate/awc/3.7.0)
|
||||
[](https://deps.rs/crate/awc/3.8.1)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## 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:
|
||||
|
||||
|
|
|
|||
|
|
@ -1037,7 +1037,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "trust-dns"))]
|
||||
#[cfg(not(feature = "hickory-dns"))]
|
||||
mod resolver {
|
||||
use super::*;
|
||||
|
||||
|
|
@ -1046,24 +1046,25 @@ mod resolver {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "trust-dns")]
|
||||
#[cfg(feature = "hickory-dns")]
|
||||
mod resolver {
|
||||
use std::{cell::RefCell, net::SocketAddr};
|
||||
use std::{cell::OnceCell, net::SocketAddr};
|
||||
|
||||
use actix_tls::connect::Resolve;
|
||||
use trust_dns_resolver::{
|
||||
use hickory_resolver::{
|
||||
config::{ResolverConfig, ResolverOpts},
|
||||
name_server::TokioConnectionProvider,
|
||||
system_conf::read_system_conf,
|
||||
TokioAsyncResolver,
|
||||
TokioResolver,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(super) fn resolver() -> Resolver {
|
||||
// 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>(
|
||||
&'a self,
|
||||
host: &'a str,
|
||||
|
|
@ -1085,34 +1086,29 @@ mod resolver {
|
|||
|
||||
// resolver struct is cached in thread local so new clients can reuse the existing instance
|
||||
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.
|
||||
TRUST_DNS_RESOLVER.with(|local| {
|
||||
let resolver = local.borrow().as_ref().map(Clone::clone);
|
||||
|
||||
match resolver {
|
||||
Some(resolver) => resolver,
|
||||
|
||||
None => {
|
||||
// get from thread local or construct a new hickory dns resolver.
|
||||
HICKORY_DNS_RESOLVER.with(|local| {
|
||||
local
|
||||
.get_or_init(|| {
|
||||
let (cfg, opts) = match read_system_conf() {
|
||||
Ok((cfg, opts)) => (cfg, opts),
|
||||
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())
|
||||
}
|
||||
};
|
||||
|
||||
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.
|
||||
let resolver = Resolver::custom(TrustDnsResolver(resolver));
|
||||
*local.borrow_mut() = Some(resolver.clone());
|
||||
|
||||
resolver
|
||||
}
|
||||
}
|
||||
Resolver::custom(HickoryDnsResolver(resolver))
|
||||
})
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ where
|
|||
|
||||
let res = poll_fn(|cx| io.poll_ready(cx)).await;
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ where
|
|||
fut.await.map_err(SendRequestError::from)?
|
||||
}
|
||||
Err(err) => {
|
||||
io.on_release(err.is_io());
|
||||
io.on_release(err.is_io() || err.is_go_away());
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@
|
|||
)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
pub use actix_http::body;
|
||||
#[cfg(feature = "cookies")]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
12
justfile
12
justfile
|
|
@ -13,10 +13,13 @@ fmt:
|
|||
[private]
|
||||
downgrade-for-msrv:
|
||||
cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0
|
||||
cargo {{ toolchain }} update -p=rayon --precise=1.10.0 # next ver: 1.80.0
|
||||
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 := ```
|
||||
cargo metadata --format-version=1 \
|
||||
|
|
@ -50,8 +53,7 @@ clippy:
|
|||
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
|
||||
|
||||
# Run Clippy over workspace using MSRV.
|
||||
clippy-msrv:
|
||||
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
|
||||
clippy-msrv: downgrade-for-msrv
|
||||
@just toolchain={{ msrv_rustup }} clippy
|
||||
|
||||
# Test workspace code.
|
||||
|
|
@ -62,8 +64,7 @@ test:
|
|||
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:
|
||||
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
|
||||
test-msrv: downgrade-for-msrv
|
||||
@just toolchain={{ msrv_rustup }} test
|
||||
|
||||
# Test workspace docs.
|
||||
|
|
@ -88,9 +89,10 @@ test-coverage-lcov: test-coverage
|
|||
cargo {{ toolchain }} llvm-cov report --doctests --lcov --output-path=lcov.info
|
||||
|
||||
# Document crates in workspace.
|
||||
# FIXME: Re-add `RUSTDOCFLAGS="--cfg=docsrs -Dwarnings"` once crypto-related crates are updated.
|
||||
doc *args: && doc-set-workspace-crates
|
||||
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]
|
||||
doc-set-workspace-crates:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# developed on macOS and probably doesn't work on Linux yet due to minor
|
||||
# differences in flags on sed
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue