Compare commits

...

23 Commits

Author SHA1 Message Date
Rob Ede 361a6e1a6c
Merge a7c5366503 into 5041cd1c65 2025-08-29 21:12:40 +01: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
Rob Ede a7c5366503
Merge branch 'master' into ci-semver-check-on-label 2024-08-10 03:17:23 +01:00
Rob Ede 270bbf1906
ci: check semver on labelling PRs 2023-12-16 11:00:29 +00:00
29 changed files with 627 additions and 286 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

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

49
.github/workflows/semver.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: Semver
on:
pull_request:
types: [labeled]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.label.name }}
cancel-in-progress: true
jobs:
check-semver-patch:
name: Check semver (patch)
runs-on: ubuntu-latest
if: github.event.label.name == 'B-semver-patch'
steps:
- uses: actions/checkout@v4
- name: Check semver (patch)
uses: obi1kenobi/cargo-semver-checks-action@v2.1
with:
release-type: patch
check-semver-minor:
name: Check semver (minor)
runs-on: ubuntu-latest
if: github.event.label.name == 'B-semver-minor'
steps:
- uses: actions/checkout@v4
- name: Check semver (minor)
uses: obi1kenobi/cargo-semver-checks-action@v2.1
with:
release-type: minor
check-semver-major:
name: Check semver (major)
runs-on: ubuntu-latest
if: github.event.label.name == 'B-semver-major'
steps:
- uses: actions/checkout@v4
- name: Check semver (major)
uses: obi1kenobi/cargo-semver-checks-action@v2.1
with:
release-type: major

481
Cargo.lock generated

File diff suppressed because it is too large Load Diff

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

@ -1182,7 +1182,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 +1216,9 @@ where
inner_p.shutdown_timer,
);
if inner_p.flags.contains(Flags::SHUTDOWN) {
cx.waker().wake_by_ref();
}
Poll::Pending
};

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

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

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

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