Compare commits

...

40 Commits

Author SHA1 Message Date
asonix ae2b6c7ec6
Merge 36a7e8cc6a into 98d7d0b46b 2025-08-29 22:48:11 +01:00
Rob Ede 98d7d0b46b
chore(actix-files): prepare release 0.6.7 2025-08-29 22:31:48 +01:00
Rob Ede 4966a54e05
refactor(files): rename read_mode_threshold fn 2025-08-29 22:30:47 +01:00
Andrew Scott 00b0f8f700
feat(actix-files): opt-in filesize threshold for faster synchronous reads (#3706)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-08-29 21:52:34 +01:00
励志买套上海苏河湾大平层 3c2907da41
docs(middleware): complete middleware author's guide (#3680)
Add comprehensive documentation for middleware development in Actix Web, including:
- Detailed explanation of middleware concepts and execution flow
- Clear description of middleware traits and their responsibilities
- Guidelines for body type handling
- Best practices for middleware development
- Error handling recommendations
- Usage scenarios and anti-patterns

Co-authored-by: chenjjiaa <chenjjiaaa@gmail.com>
2025-08-29 20:12:05 +00:00
George Pollard 5041cd1c65
Make 'ws' feature of actix-http optional in actix-web (#3734)
* Make 'ws' feature of actix-http optional

* Update CHANGES.md

* Update actix-web-actors

* Update CHANGES.md

* nits

* nits

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-08-29 02:50:05 +00:00
Thales d3c46537b3
fix(http): Wake Payload when feeding error or EOF (#3749)
* fix(http): Add failing tests to demonstrate the payload problem

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

* fix(http): Wake Payload when feeding error or eof

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

---------

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>
2025-08-29 02:47:03 +00:00
Rob Ede 8996198f2c
chore: require h2 versions after MadeYouReset fix 2025-08-26 23:59:57 +01:00
Rob Ede 68624ec63b
chore: remove now-useless docs.rs flags 2025-08-26 23:51:22 +01:00
Rob Ede bcd0ffb016
chore: add multi-crate publish script 2025-08-26 09:25:22 +01:00
Rob Ede 9fb6c13a1a
ci: fix msrv job 2025-08-26 08:26:49 +01:00
Rob Ede 05cfef7f4b
ci: fix msrv job 2025-08-26 08:18:34 +01:00
Rob Ede 8f3eb32a32
chore: fix justfile for msrv 2025-08-26 08:00:19 +01:00
Rob Ede ddd16ec9db
chore(actix-http): prepare release 3.11.1 2025-08-26 07:28:27 +01:00
dependabot[bot] 9c47a247fb
build(deps): bump bitflags from 2.9.2 to 2.9.3 (#3745)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.9.2 to 2.9.3.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.9.2...2.9.3)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 12:31:30 +00:00
dependabot[bot] 2536823e3b
build(deps): bump regex-lite from 0.1.6 to 0.1.7 (#3743)
Bumps [regex-lite](https://github.com/rust-lang/regex) from 0.1.6 to 0.1.7.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/regex-lite-0.1.6...regex-lite-0.1.7)

---
updated-dependencies:
- dependency-name: regex-lite
  dependency-version: 0.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:43:50 +00:00
dependabot[bot] e3f81d0643
build(deps): bump url from 2.5.4 to 2.5.7 (#3742)
Bumps [url](https://github.com/servo/rust-url) from 2.5.4 to 2.5.7.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/commits)

---
updated-dependencies:
- dependency-name: url
  dependency-version: 2.5.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:42:00 +00:00
dependabot[bot] a84aee836b
build(deps): bump tempfile from 3.20.0 to 3.21.0 (#3744)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.20.0 to 3.21.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/commits)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:40:55 +00:00
dependabot[bot] 1f08cb24c3
build(deps): bump regex from 1.11.1 to 1.11.2 (#3746)
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.1 to 1.11.2.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.1...1.11.2)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:36:40 +00:00
dependabot[bot] 1b49047086
build(deps): bump codecov/codecov-action from 5.4.3 to 5.5.0 (#3741)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.3 to 5.5.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](18283e04ce...fdcc847654)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:33:15 +00:00
dependabot[bot] 9fe033a963
build(deps): bump taiki-e/install-action from 2.58.17 to 2.58.21 (#3740)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.58.17 to 2.58.21.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](ad95d4e02e...f63c33fd96)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.58.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:32:36 +00:00
dependabot[bot] 2ba69a1904
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.13.0 to 1.14.0 (#3739)
build(deps): bump actions-rust-lang/setup-rust-toolchain

Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](fb51252c7b...ab6845274e)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:31:31 +00:00
Andrew Scott c6352005f7
fix: include content-length with bytes payload (#3695)
* fix: include content-length with bytes payload

* chore: json unit-test patch

* Update doc comment

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2025-08-24 09:42:11 +00:00
Yuki Okushi aeb42486af
fix(actix-web): improve streaming response behavior for Content-Length and Content-Type (#3737)
- Set default Content-Type to application/octet-stream for streaming responses
- Respect Content-Length header by automatically calling `no_chunking()` when set
- Resolves issue where Content-Length was ignored in streaming responses

Fixes #2306

---------

Co-authored-by: Yinuo Deng <i@dyn.im>
2025-08-23 12:23:57 +09:00
yanyuxing 30f7df5853
fix(actix-http): wake before return pending when read half disconnect and need to shutdown (#3665) 2025-08-23 00:56:39 +00:00
asonix 36a7e8cc6a Don't reserve unless we need to 2024-11-04 17:58:37 -06:00
asonix 5e28e69e29 Merge branch 'master' into asonix/play-with-h1-encoding 2024-11-04 17:36:28 -06:00
asonix ab04286b01 Ensure bytes >=1.7 2024-11-04 17:33:02 -06:00
asonix 627d113323 Expand BigBytes usage to ws encoding 2024-11-04 13:06:47 -06:00
asonix 94c5d4d641 Drop buffers in clear if 'too big' 2024-05-19 19:24:37 -05:00
asonix 43fca317d3
Merge branch 'master' into asonix/play-with-h1-encoding 2024-05-19 17:13:33 -05:00
asonix 69ca0e7c57 Separate medium & large, make large bigger 2024-05-19 17:06:05 -05:00
asonix a94b5b89a5 Add other endpoints to actix web example 2024-05-19 16:52:59 -05:00
asonix 42bd5eebdb Add buffer heuristic 2024-05-19 16:49:10 -05:00
asonix 8eb1d10bae Don't add empty bytes to queue 2024-05-19 16:17:44 -05:00
asonix 96f5ebb549 Simplify bigbytes - always put bytes into queue 2024-05-19 16:08:55 -05:00
asonix a6b5c9893d Make fmt lint happy 2024-05-18 13:14:10 -05:00
asonix 09b460c72e Promote BigBytes from pub(super) to pub(crate) 2024-05-18 12:53:41 -05:00
asonix b492b27e4a clippy 2024-05-18 12:38:07 -05:00
asonix 35cadbbe0b actix-http: play with allowing bytes passthrough in the h1 encoder 2024-05-18 12:30:19 -05:00
44 changed files with 1105 additions and 410 deletions

View File

@ -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 },
]

View File

@ -9,4 +9,5 @@ words:
- rustls
- rustup
- serde
- uring
- zstd

View File

@ -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@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -80,10 +80,10 @@ jobs:
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
- name: Install just, cargo-hack
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just,cargo-hack

View File

@ -59,12 +59,12 @@ jobs:
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -92,7 +92,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
@ -108,12 +108,12 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
- name: Install just
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just

View File

@ -18,13 +18,13 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
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@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
with:
files: codecov.json
fail_ci_if_error: true

View File

@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
components: rustfmt
@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
components: clippy
@ -55,7 +55,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
components: rust-docs
@ -72,12 +72,12 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just

502
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,9 @@
## Unreleased
## 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

View File

@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.6.6"
version = "0.6.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "Static file serving for Actix Web"
keywords = ["actix", "http", "async", "futures"]

View File

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

View File

@ -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,31 +77,50 @@ 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);
let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?;
file.seek(io::SeekFrom::Start(offset))?;
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
Ok((file, Bytes::from(buf)))
if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
Ok((file, Bytes::from(buf)))
}
}
#[cfg(not(feature = "experimental-io-uring"))]
#[inline]
async fn chunked_read_file_callback(
file: File,
offset: u64,
max_bytes: usize,
read_mode: ReadMode,
) -> Result<(File, Bytes), Error> {
let res = match read_mode {
ReadMode::Sync => chunked_read_file_callback_sync(file, offset, max_bytes)?,
ReadMode::Async => {
actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes))
.await??
}
})
.await??;
};
Ok(res)
}
@ -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 });

View File

@ -49,6 +49,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 {
@ -73,6 +74,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,
}
}
}
@ -119,6 +121,7 @@ impl Files {
use_guards: None,
guards: Vec::new(),
hidden_files: false,
read_mode_threshold: 0,
}
}
@ -204,6 +207,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 +387,7 @@ 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,
};
if let Some(ref default) = *self.default.borrow() {

View File

@ -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);

View File

@ -39,6 +39,7 @@ 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,
}
impl fmt::Debug for FilesServiceInner {
@ -70,7 +71,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)
}
@ -169,17 +172,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,
}
}

View File

@ -2,7 +2,13 @@
## Unreleased
- Malformed websocket frames are now gracefully rejected.
- Properly wake Payload receivers when feeding errors or EOF
## 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

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "3.11.0"
version = "3.11.1"
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",
@ -99,7 +98,7 @@ actix-service = "2"
actix-utils = "3"
bitflags = "2"
bytes = "1"
bytes = "1.7"
bytestring = "1"
derive_more = { version = "2", features = ["as_ref", "deref", "deref_mut", "display", "error", "from"] }
encoding_rs = "0.8"
@ -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

View File

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

View File

@ -1,19 +1,34 @@
use std::sync::OnceLock;
use actix_http::HttpService;
use actix_server::Server;
use actix_service::map_config;
use actix_web::{dev::AppConfig, get, App, Responder};
static MEDIUM: OnceLock<String> = OnceLock::new();
static LARGE: OnceLock<String> = OnceLock::new();
#[get("/")]
async fn index() -> impl Responder {
"Hello, world. From Actix Web!"
}
#[get("/large")]
async fn large() -> &'static str {
LARGE.get_or_init(|| "123456890".repeat(1024 * 100))
}
#[get("/medium")]
async fn medium() -> &'static str {
MEDIUM.get_or_init(|| "123456890".repeat(1024 * 5))
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> std::io::Result<()> {
Server::build()
.bind("hello-world", "127.0.0.1:8080", || {
// construct actix-web app
let app = App::new().service(index);
let app = App::new().service(index).service(large).service(medium);
HttpService::build()
// pass the app to service builder

124
actix-http/src/big_bytes.rs Normal file
View File

@ -0,0 +1,124 @@
use std::collections::VecDeque;
use bytes::{Buf, BufMut, Bytes, BytesMut};
// 64KB max capacity (arbitrarily chosen)
const MAX_CAPACITY: usize = 1024 * 64;
pub struct BigBytes {
buffer: BytesMut,
frozen: VecDeque<Bytes>,
frozen_len: usize,
}
impl BigBytes {
/// Initialize a new BigBytes with the internal buffer set to `capacity` capacity
pub fn with_capacity(capacity: usize) -> Self {
Self {
buffer: BytesMut::with_capacity(capacity),
frozen: VecDeque::default(),
frozen_len: 0,
}
}
/// Clear the internal queue and buffer, resetting length to zero
///
/// if the internal buffer capacity exceeds 64KB or new_capacity, whichever is greater, it will
/// be freed and a new buffer of capacity `new_capacity` will be allocated
pub fn clear(&mut self, new_capacity: usize) {
std::mem::take(&mut self.frozen);
self.frozen_len = 0;
self.buffer.clear();
if self.buffer.capacity() > new_capacity.max(MAX_CAPACITY) {
self.buffer = BytesMut::with_capacity(new_capacity);
}
}
/// Return a mutable reference to the underlying buffer. This should only be used when dealing
/// with small allocations (e.g. writing headers)
pub fn buffer_mut(&mut self) -> &mut BytesMut {
&mut self.buffer
}
/// Return the total length of the bytes stored in BigBytes
pub fn total_len(&mut self) -> usize {
self.frozen_len + self.buffer.len()
}
/// Return whether there are no bytes present in the BigBytes
pub fn is_empty(&self) -> bool {
self.frozen_len == 0 && self.buffer.is_empty()
}
/// Add the `bytes` to the internal structure. If `bytes` exceeds 64KB, it is pushed into a
/// queue, otherwise, it is added to a buffer.
pub fn put_bytes(&mut self, bytes: Bytes) {
if !self.buffer.is_empty() {
let current = self.buffer.split().freeze();
self.frozen_len += current.len();
self.frozen.push_back(current);
}
if !bytes.is_empty() {
self.frozen_len += bytes.len();
self.frozen.push_back(bytes);
}
}
/// Returns a slice of the frontmost buffer
///
/// While there are bytes present in BigBytes, front_slice is guaranteed not to return an empty
/// slice.
pub fn front_slice(&self) -> &[u8] {
if let Some(front) = self.frozen.front() {
front
} else {
&self.buffer
}
}
/// Advances the first buffer by `count` bytes. If the first buffer is advanced to completion,
/// it is popped from the queue
pub fn advance(&mut self, count: usize) {
if let Some(front) = self.frozen.front_mut() {
front.advance(count);
if front.is_empty() {
self.frozen.pop_front();
}
self.frozen_len -= count;
} else {
self.buffer.advance(count);
}
}
/// Pops the front Bytes from the BigBytes, or splits and freezes the internal buffer if no
/// Bytes are present.
pub fn pop_front(&mut self) -> Option<Bytes> {
if let Some(front) = self.frozen.pop_front() {
self.frozen_len -= front.len();
Some(front)
} else if !self.buffer.is_empty() {
Some(self.buffer.split().freeze())
} else {
None
}
}
/// Drain the BigBytes, writing everything into the provided BytesMut
pub fn write_to(&mut self, dst: &mut BytesMut) {
dst.reserve(self.total_len());
for buf in &self.frozen {
dst.put_slice(buf);
}
dst.put_slice(&self.buffer.split());
self.frozen_len = 0;
std::mem::take(&mut self.frozen);
}
}

View File

@ -9,7 +9,10 @@ use super::{
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
encoder, Message, MessageType,
};
use crate::{body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig};
use crate::{
big_bytes::BigBytes, body::BodySize, error::ParseError, ConnectionType, Request, Response,
ServiceConfig,
};
bitflags! {
#[derive(Debug, Clone, Copy)]
@ -146,14 +149,12 @@ impl Decoder for Codec {
}
}
impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
type Error = io::Error;
fn encode(
impl Codec {
pub(super) fn encode_bigbytes(
&mut self,
item: Message<(Response<()>, BodySize)>,
dst: &mut BytesMut,
) -> Result<(), Self::Error> {
dst: &mut BigBytes,
) -> std::io::Result<()> {
match item {
Message::Item((mut res, length)) => {
// set response version
@ -172,7 +173,7 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
// encode message
self.encoder.encode(
dst,
dst.buffer_mut(),
&mut res,
self.flags.contains(Flags::HEAD),
self.flags.contains(Flags::STREAM),
@ -184,11 +185,11 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
}
Message::Chunk(Some(bytes)) => {
self.encoder.encode_chunk(bytes.as_ref(), dst)?;
self.encoder.encode_chunk_bigbytes(bytes, dst)?;
}
Message::Chunk(None) => {
self.encoder.encode_eof(dst)?;
self.encoder.encode_eof(dst.buffer_mut())?;
}
}
@ -196,6 +197,23 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
}
}
impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
type Error = io::Error;
fn encode(
&mut self,
item: Message<(Response<()>, BodySize)>,
dst: &mut BytesMut,
) -> Result<(), Self::Error> {
let mut bigbytes = BigBytes::with_capacity(1024 * 8);
self.encode_bigbytes(item, &mut bigbytes)?;
bigbytes.write_to(dst);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -12,11 +12,11 @@ use actix_codec::{Framed, FramedParts};
use actix_rt::time::sleep_until;
use actix_service::Service;
use bitflags::bitflags;
use bytes::{Buf, BytesMut};
use bytes::BytesMut;
use futures_core::ready;
use pin_project_lite::pin_project;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::codec::{Decoder as _, Encoder as _};
use tokio_util::codec::Decoder as _;
use tracing::{error, trace};
use super::{
@ -27,6 +27,7 @@ use super::{
Message, MessageType,
};
use crate::{
big_bytes::BigBytes,
body::{BodySize, BoxBody, MessageBody},
config::ServiceConfig,
error::{DispatchError, ParseError, PayloadError},
@ -165,7 +166,7 @@ pin_project! {
pub(super) io: Option<T>,
read_buf: BytesMut,
write_buf: BytesMut,
write_buf: BigBytes,
codec: Codec,
}
}
@ -277,7 +278,7 @@ where
io: Some(io),
read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
write_buf: BigBytes::with_capacity(HW_BUFFER_SIZE),
codec: Codec::new(config),
},
},
@ -329,27 +330,24 @@ where
let InnerDispatcherProj { io, write_buf, .. } = self.project();
let mut io = Pin::new(io.as_mut().unwrap());
let len = write_buf.len();
let mut written = 0;
while written < len {
match io.as_mut().poll_write(cx, &write_buf[written..])? {
while write_buf.total_len() > 0 {
match io.as_mut().poll_write(cx, write_buf.front_slice())? {
Poll::Ready(0) => {
println!("WRITE ZERO");
error!("write zero; closing");
return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, "")));
}
Poll::Ready(n) => written += n,
Poll::Ready(n) => write_buf.advance(n),
Poll::Pending => {
write_buf.advance(written);
return Poll::Pending;
}
}
}
// everything has written to I/O; clear buffer
write_buf.clear();
write_buf.clear(HW_BUFFER_SIZE);
// flush the I/O and check if get blocked
io.poll_flush(cx)
@ -365,7 +363,7 @@ where
let size = body.size();
this.codec
.encode(Message::Item((res, size)), this.write_buf)
.encode_bigbytes(Message::Item((res, size)), this.write_buf)
.map_err(|err| {
if let Some(mut payload) = this.payload.take() {
payload.set_error(PayloadError::Incomplete(None));
@ -416,6 +414,7 @@ where
fn send_continue(self: Pin<&mut Self>) {
self.project()
.write_buf
.buffer_mut()
.extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n");
}
@ -493,15 +492,16 @@ where
StateProj::SendPayload { mut body } => {
// keep populate writer buffer until buffer size limit hit,
// get blocked or finished.
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
while this.write_buf.total_len() < super::payload::MAX_BUFFER_SIZE {
match body.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => {
this.codec
.encode(Message::Chunk(Some(item)), this.write_buf)?;
.encode_bigbytes(Message::Chunk(Some(item)), this.write_buf)?;
}
Poll::Ready(None) => {
this.codec.encode(Message::Chunk(None), this.write_buf)?;
this.codec
.encode_bigbytes(Message::Chunk(None), this.write_buf)?;
// payload stream finished.
// set state to None and handle next message
@ -532,15 +532,16 @@ where
// keep populate writer buffer until buffer size limit hit,
// get blocked or finished.
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
while this.write_buf.total_len() < super::payload::MAX_BUFFER_SIZE {
match body.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => {
this.codec
.encode(Message::Chunk(Some(item)), this.write_buf)?;
.encode_bigbytes(Message::Chunk(Some(item)), this.write_buf)?;
}
Poll::Ready(None) => {
this.codec.encode(Message::Chunk(None), this.write_buf)?;
this.codec
.encode_bigbytes(Message::Chunk(None), this.write_buf)?;
// payload stream finished
// set state to None and handle next message
@ -575,6 +576,7 @@ where
// to service call.
Poll::Ready(Ok(req)) => {
this.write_buf
.buffer_mut()
.extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n");
let fut = this.flow.service.call(req);
this.state.set(State::ServiceCall { fut });
@ -1027,7 +1029,7 @@ where
mem::take(this.codec),
mem::take(this.read_buf),
);
parts.write_buf = mem::take(this.write_buf);
this.write_buf.write_to(&mut parts.write_buf);
let framed = Framed::from_parts(parts);
this.flow.upgrade.as_ref().unwrap().call((req, framed))
}
@ -1182,7 +1184,7 @@ where
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 inner_p.flags.contains(Flags::READ_DISCONNECT) {
trace!("read half closed; start shutdown");
inner_p.flags.insert(Flags::SHUTDOWN);
}
@ -1216,6 +1218,9 @@ where
inner_p.shutdown_timer,
);
if inner_p.flags.contains(Flags::SHUTDOWN) {
cx.waker().wake_by_ref();
}
Poll::Pending
};

View File

@ -6,9 +6,10 @@ use std::{
slice::from_raw_parts_mut,
};
use bytes::{BufMut, BytesMut};
use bytes::{BufMut, Bytes, BytesMut};
use crate::{
big_bytes::BigBytes,
body::BodySize,
header::{
map::Value, HeaderMap, HeaderName, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING,
@ -323,6 +324,14 @@ impl<T: MessageType> MessageEncoder<T> {
self.te.encode(msg, buf)
}
pub(super) fn encode_chunk_bigbytes(
&mut self,
msg: Bytes,
buf: &mut BigBytes,
) -> io::Result<bool> {
self.te.encode_bigbytes(msg, buf)
}
/// Encode EOF.
pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> {
self.te.encode_eof(buf)
@ -414,6 +423,63 @@ impl TransferEncoding {
}
}
#[inline]
/// Encode message. Return `EOF` state of encoder
pub(super) fn encode_bigbytes(&mut self, msg: Bytes, buf: &mut BigBytes) -> io::Result<bool> {
match self.kind {
TransferEncodingKind::Eof => {
let eof = msg.is_empty();
if msg.len() > 1024 * 64 {
buf.put_bytes(msg);
} else {
buf.buffer_mut().extend_from_slice(&msg);
}
Ok(eof)
}
TransferEncodingKind::Chunked(ref mut eof) => {
if *eof {
return Ok(true);
}
if msg.is_empty() {
*eof = true;
buf.buffer_mut().extend_from_slice(b"0\r\n\r\n");
} else {
writeln!(helpers::MutWriter(buf.buffer_mut()), "{:X}\r", msg.len())
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
if msg.len() > 1024 * 64 {
buf.put_bytes(msg);
} else {
buf.buffer_mut().reserve(msg.len() + 2);
buf.buffer_mut().extend_from_slice(&msg);
}
buf.buffer_mut().extend_from_slice(b"\r\n");
}
Ok(*eof)
}
TransferEncodingKind::Length(ref mut remaining) => {
if *remaining > 0 {
if msg.is_empty() {
return Ok(*remaining == 0);
}
let len = cmp::min(*remaining, msg.len() as u64);
if len > 1024 * 64 {
buf.put_bytes(msg.slice(..len as usize));
} else {
buf.buffer_mut().extend_from_slice(&msg[..len as usize]);
}
*remaining -= len;
Ok(*remaining == 0)
} else {
Ok(true)
}
}
}
}
/// Encode message. Return `EOF` state of encoder
#[inline]
pub fn encode(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result<bool> {

View File

@ -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);

View File

@ -31,6 +31,7 @@
pub use http::{uri, uri::Uri, Method, StatusCode, Version};
pub mod big_bytes;
pub mod body;
mod builder;
mod config;

View File

@ -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
}

View File

@ -4,6 +4,8 @@ use bytestring::ByteString;
use tokio_util::codec::{Decoder, Encoder};
use tracing::error;
use crate::big_bytes::BigBytes;
use super::{
frame::Parser,
proto::{CloseReason, OpCode},
@ -116,51 +118,55 @@ impl Default for Codec {
}
}
impl Encoder<Message> for Codec {
type Error = ProtocolError;
fn encode(&mut self, item: Message, dst: &mut BytesMut) -> Result<(), Self::Error> {
impl Codec {
pub fn encode_bigbytes(
&mut self,
item: Message,
dst: &mut BigBytes,
) -> Result<(), ProtocolError> {
match item {
Message::Text(txt) => Parser::write_message(
Message::Text(txt) => Parser::write_message_bigbytes(
dst,
txt,
txt.into_bytes(),
OpCode::Text,
true,
!self.flags.contains(Flags::SERVER),
),
Message::Binary(bin) => Parser::write_message(
Message::Binary(bin) => Parser::write_message_bigbytes(
dst,
bin,
OpCode::Binary,
true,
!self.flags.contains(Flags::SERVER),
),
Message::Ping(txt) => Parser::write_message(
Message::Ping(txt) => Parser::write_message_bigbytes(
dst,
txt,
OpCode::Ping,
true,
!self.flags.contains(Flags::SERVER),
),
Message::Pong(txt) => Parser::write_message(
Message::Pong(txt) => Parser::write_message_bigbytes(
dst,
txt,
OpCode::Pong,
true,
!self.flags.contains(Flags::SERVER),
),
Message::Close(reason) => {
Parser::write_close(dst, reason, !self.flags.contains(Flags::SERVER))
}
Message::Close(reason) => Parser::write_close(
dst.buffer_mut(),
reason,
!self.flags.contains(Flags::SERVER),
),
Message::Continuation(cont) => match cont {
Item::FirstText(data) => {
if self.flags.contains(Flags::W_CONTINUATION) {
return Err(ProtocolError::ContinuationStarted);
} else {
self.flags.insert(Flags::W_CONTINUATION);
Parser::write_message(
Parser::write_message_bigbytes(
dst,
&data[..],
data,
OpCode::Text,
false,
!self.flags.contains(Flags::SERVER),
@ -172,9 +178,9 @@ impl Encoder<Message> for Codec {
return Err(ProtocolError::ContinuationStarted);
} else {
self.flags.insert(Flags::W_CONTINUATION);
Parser::write_message(
Parser::write_message_bigbytes(
dst,
&data[..],
data,
OpCode::Binary,
false,
!self.flags.contains(Flags::SERVER),
@ -183,9 +189,9 @@ impl Encoder<Message> for Codec {
}
Item::Continue(data) => {
if self.flags.contains(Flags::W_CONTINUATION) {
Parser::write_message(
Parser::write_message_bigbytes(
dst,
&data[..],
data,
OpCode::Continue,
false,
!self.flags.contains(Flags::SERVER),
@ -197,9 +203,9 @@ impl Encoder<Message> for Codec {
Item::Last(data) => {
if self.flags.contains(Flags::W_CONTINUATION) {
self.flags.remove(Flags::W_CONTINUATION);
Parser::write_message(
Parser::write_message_bigbytes(
dst,
&data[..],
data,
OpCode::Continue,
true,
!self.flags.contains(Flags::SERVER),
@ -215,6 +221,20 @@ impl Encoder<Message> for Codec {
}
}
impl Encoder<Message> for Codec {
type Error = ProtocolError;
fn encode(&mut self, item: Message, dst: &mut BytesMut) -> Result<(), Self::Error> {
let mut big_bytes = BigBytes::with_capacity(0);
self.encode_bigbytes(item, &mut big_bytes)?;
big_bytes.write_to(dst);
Ok(())
}
}
impl Decoder for Codec {
type Item = Frame;
type Error = ProtocolError;

View File

@ -1,8 +1,10 @@
use std::cmp::min;
use bytes::{Buf, BufMut, BytesMut};
use bytes::{Buf, BufMut, Bytes, BytesMut};
use tracing::debug;
use crate::big_bytes::BigBytes;
use super::{
mask::apply_mask,
proto::{CloseCode, CloseReason, OpCode},
@ -166,51 +168,68 @@ impl Parser {
}
}
/// Generate binary representation
pub fn write_message<B: AsRef<[u8]>>(
dst: &mut BytesMut,
pl: B,
pub fn write_message_bigbytes(
dst: &mut BigBytes,
pl: Bytes,
op: OpCode,
fin: bool,
mask: bool,
) {
let payload = pl.as_ref();
let one: u8 = if fin {
0x80 | Into::<u8>::into(op)
} else {
op.into()
};
let payload_len = payload.len();
let (two, p_len) = if mask {
(0x80, payload_len + 4)
} else {
(0, payload_len)
};
let payload_len = pl.len();
let two = if mask { 0x80 } else { 0 };
if payload_len < 126 {
dst.reserve(p_len + 2);
dst.put_slice(&[one, two | payload_len as u8]);
dst.buffer_mut().reserve(2);
dst.buffer_mut().put_slice(&[one, two | payload_len as u8]);
} else if payload_len <= 65_535 {
dst.reserve(p_len + 4);
dst.put_slice(&[one, two | 126]);
dst.put_u16(payload_len as u16);
dst.buffer_mut().reserve(4);
dst.buffer_mut().put_slice(&[one, two | 126]);
dst.buffer_mut().put_u16(payload_len as u16);
} else {
dst.reserve(p_len + 10);
dst.put_slice(&[one, two | 127]);
dst.put_u64(payload_len as u64);
dst.buffer_mut().reserve(10);
dst.buffer_mut().put_slice(&[one, two | 127]);
dst.buffer_mut().put_u64(payload_len as u64);
};
if mask {
let mask = rand::random::<[u8; 4]>();
dst.put_slice(mask.as_ref());
dst.put_slice(payload.as_ref());
let pos = dst.len() - payload_len;
apply_mask(&mut dst[pos..], mask);
dst.buffer_mut().put_slice(mask.as_ref());
match pl.try_into_mut() {
// Avoid copying bytes by mutating in-place
Ok(mut pl_mut) => {
apply_mask(&mut pl_mut, mask);
dst.put_bytes(pl_mut.freeze());
}
// We need to copy the bytes anyway at this point, so put them in the buffer
// directly
Err(pl) => {
dst.buffer_mut().reserve(pl.len());
dst.buffer_mut().put_slice(pl.as_ref());
let pos = dst.buffer_mut().len() - payload_len;
apply_mask(&mut dst.buffer_mut()[pos..], mask);
}
}
} else {
dst.put_slice(payload.as_ref());
dst.put_bytes(pl)
}
}
/// Generate binary representation
pub fn write_message(dst: &mut BytesMut, pl: Bytes, op: OpCode, fin: bool, mask: bool) {
let mut big_bytes = BigBytes::with_capacity(0);
Self::write_message_bigbytes(&mut big_bytes, pl, op, fin, mask);
big_bytes.write_to(dst);
}
/// Create a new Close control frame.
#[inline]
pub fn write_close(dst: &mut BytesMut, reason: Option<CloseReason>, mask: bool) {
@ -225,7 +244,7 @@ impl Parser {
}
};
Parser::write_message(dst, payload, OpCode::Close, true, mask)
Parser::write_message(dst, Bytes::from(payload), OpCode::Close, true, mask)
}
}
@ -378,7 +397,13 @@ mod tests {
#[test]
fn test_ping_frame() {
let mut buf = BytesMut::new();
Parser::write_message(&mut buf, Vec::from("data"), OpCode::Ping, true, false);
Parser::write_message(
&mut buf,
Bytes::from(Vec::from("data")),
OpCode::Ping,
true,
false,
);
let mut v = vec![137u8, 4u8];
v.extend(b"data");
@ -388,7 +413,13 @@ mod tests {
#[test]
fn test_pong_frame() {
let mut buf = BytesMut::new();
Parser::write_message(&mut buf, Vec::from("data"), OpCode::Pong, true, false);
Parser::write_message(
&mut buf,
Bytes::from(Vec::from("data")),
OpCode::Pong,
true,
false,
);
let mut v = vec![138u8, 4u8];
v.extend(b"data");

View File

@ -11,7 +11,6 @@ edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[lib]

View File

@ -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]

View File

@ -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),

View File

@ -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"

View File

@ -2,6 +2,10 @@
## Unreleased
- `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()` 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.
## 4.11.0
- Add `Logger::log_level()` method.

View File

@ -17,7 +17,6 @@ edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [
"macros",
"openssl",
@ -68,6 +67,7 @@ default = [
"http2",
"unicode",
"compat",
"ws",
]
# Brotli algorithm content-encoding support
@ -86,9 +86,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"]
@ -132,7 +135,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 }

View File

@ -3,7 +3,6 @@
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
- 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.

View File

@ -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::*;

View File

@ -2,16 +2,80 @@
## 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

View File

@ -318,12 +318,33 @@ 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));
}
}
if let Some(parts) = self.inner() {
if let Some(length) = parts.headers.get(header::CONTENT_LENGTH) {
if let Ok(length) = length.to_str() {
if let Ok(length) = length.parse::<u64>() {
self.no_chunking(length);
}
}
}
}
self.body(BodyStream::new(stream))
}

View File

@ -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;

View File

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

View File

@ -16,7 +16,6 @@ license = "MIT OR Apache-2.0"
edition = "2021"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [
"cookies",
"openssl",
@ -109,7 +108,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"

View File

@ -13,6 +13,8 @@ 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
@ -50,8 +52,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 +63,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.

View File

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

View File

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

25
scripts/publish Executable file
View File

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