Merge branch 'master' into asonix/shutdown-when-not-reading-full-request

This commit is contained in:
Rob Ede 2025-08-29 21:47:44 +01:00 committed by GitHub
commit b1ea42cbef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 743 additions and 337 deletions

View File

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

2
.github/FUNDING.yml vendored
View File

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

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust - name: Install Rust
run: | run: |

View File

@ -28,11 +28,11 @@ jobs:
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install nasm - name: Install nasm
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.2 uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
@ -44,12 +44,12 @@ jobs:
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.57.5 uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -71,19 +71,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Free Disk Space - name: Free Disk Space
run: ./scripts/free-disk-space.sh run: ./scripts/free-disk-space.sh
- name: Setup mold linker - name: Setup mold linker
uses: rui314/setup-mold@v1 uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
- name: Install just, cargo-hack - name: Install just, cargo-hack
uses: taiki-e/install-action@v2.57.5 uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with: with:
tool: just,cargo-hack tool: just,cargo-hack

View File

@ -18,7 +18,7 @@ concurrency:
jobs: jobs:
read_msrv: read_msrv:
name: Read MSRV name: Read MSRV
uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0 uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@8b553824444060021f2843d7b4d803f3624d15e5 # v0.1.0
build_and_test: build_and_test:
needs: read_msrv needs: read_msrv
@ -39,11 +39,11 @@ jobs:
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install nasm - name: Install nasm
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.2 uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
@ -56,15 +56,15 @@ jobs:
- name: Setup mold linker - name: Setup mold linker
if: matrix.target.os == 'ubuntu-latest' if: matrix.target.os == 'ubuntu-latest'
uses: rui314/setup-mold@v1 uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.57.5 uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -89,10 +89,10 @@ jobs:
name: io-uring tests name: io-uring tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with: with:
toolchain: nightly toolchain: nightly
@ -105,15 +105,15 @@ jobs:
name: doc tests name: doc tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with: with:
toolchain: nightly toolchain: nightly
- name: Install just - name: Install just
uses: taiki-e/install-action@v2.57.5 uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with: with:
tool: just tool: just

View File

@ -15,16 +15,16 @@ jobs:
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with: with:
toolchain: nightly toolchain: nightly
components: llvm-tools components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest - name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@v2.57.5 uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with: with:
tool: just,cargo-llvm-cov,cargo-nextest tool: just,cargo-llvm-cov,cargo-nextest
@ -32,7 +32,7 @@ jobs:
run: just test-coverage-codecov run: just test-coverage-codecov
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.3 uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
with: with:
files: codecov.json files: codecov.json
fail_ci_if_error: true fail_ci_if_error: true

View File

@ -15,10 +15,10 @@ jobs:
fmt: fmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with: with:
toolchain: nightly toolchain: nightly
components: rustfmt components: rustfmt
@ -33,15 +33,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with: with:
components: clippy components: clippy
- name: Check with Clippy - name: Check with Clippy
uses: giraffate/clippy-action@v1.0.1 uses: giraffate/clippy-action@13b9d32482f25d29ead141b79e7e04e7900281e0 # v1.0.1
with: with:
reporter: github-pr-check reporter: github-pr-check
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
@ -52,10 +52,10 @@ jobs:
lint-docs: lint-docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with: with:
toolchain: nightly toolchain: nightly
components: rust-docs components: rust-docs
@ -69,20 +69,20 @@ jobs:
if: false # rustdoc mismatch currently if: false # rustdoc mismatch currently
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }}) - name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0 uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with: with:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just - name: Install just
uses: taiki-e/install-action@v2.57.5 uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with: with:
tool: just tool: just
- name: Install cargo-check-external-types - name: Install cargo-check-external-types
uses: taiki-e/cache-cargo-install-action@v2.2.0 uses: taiki-e/cache-cargo-install-action@b33c63d3b3c85540f4eba8a4f71a5cc0ce030855 # v2.3.0
with: with:
tool: cargo-check-external-types tool: cargo-check-external-types

497
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,14 @@
## Unreleased ## Unreleased
- 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 ## 3.11.0
- Update `brotli` dependency to `8`. - Update `brotli` dependency to `8`.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.11.0" version = "3.11.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "HTTP types and services for the Actix ecosystem" description = "HTTP types and services for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
@ -17,7 +17,6 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [ features = [
"http2", "http2",
"ws", "ws",
@ -119,7 +118,7 @@ tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# http2 # http2
h2 = { version = "0.3.26", optional = true } h2 = { version = "0.3.27", optional = true }
# websockets # websockets
base64 = { version = "0.22", optional = true } base64 = { version = "0.22", optional = true }
@ -157,7 +156,7 @@ serde_json = "1.0"
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.55" } tls-openssl = { package = "openssl", version = "0.10.55" }
tls-rustls_023 = { package = "rustls", version = "0.23" } tls-rustls_023 = { package = "rustls", version = "0.23" }
tokio = { version = "1.38.2", features = ["net", "rt", "macros"] } tokio = { version = "1.38.2", features = ["net", "rt", "macros", "sync"] }
[lints] [lints]
workspace = true workspace = true

View File

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

View File

@ -1226,7 +1226,7 @@ where
let state_is_none = inner_p.state.is_none(); let state_is_none = inner_p.state.is_none();
// read half is closed; we do not process any responses // 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"); trace!("read half closed; start shutdown");
inner_p.flags.insert(Flags::SHUTDOWN); inner_p.flags.insert(Flags::SHUTDOWN);
} }
@ -1260,6 +1260,9 @@ where
inner_p.shutdown_timer, inner_p.shutdown_timer,
); );
if inner_p.flags.contains(Flags::SHUTDOWN) {
cx.waker().wake_by_ref();
}
Poll::Pending Poll::Pending
}; };

View File

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

View File

@ -11,7 +11,7 @@ use std::{
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::{Method, Uri, Version}; use http::{header, Method, Uri, Version};
use crate::{ use crate::{
header::{HeaderMap, TryIntoHeaderPair}, header::{HeaderMap, TryIntoHeaderPair},
@ -98,9 +98,13 @@ impl TestRequest {
} }
/// Set request payload. /// Set request payload.
///
/// This sets the `Content-Length` header with the size of `data`.
pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self { pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self {
let mut payload = crate::h1::Payload::empty(); let mut payload = crate::h1::Payload::empty();
payload.unread_data(data.into()); let bytes = data.into();
self.insert_header((header::CONTENT_LENGTH, bytes.len()));
payload.unread_data(bytes);
parts(&mut self.0).payload = Some(payload.into()); parts(&mut self.0).payload = Some(payload.into());
self self
} }

View File

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

View File

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

View File

@ -14,7 +14,6 @@ license.workspace = true
edition.workspace = true edition.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true all-features = true
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]

View File

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

View File

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

View File

@ -13,7 +13,7 @@
//! ```no_run //! ```no_run
//! use actix_web::{post, App, HttpServer, Responder}; //! use actix_web::{post, App, HttpServer, Responder};
//! //!
//! use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; //! use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm, MultipartFormConfig};
//! use serde::Deserialize; //! use serde::Deserialize;
//! //!
//! #[derive(Debug, Deserialize)] //! #[derive(Debug, Deserialize)]
@ -23,6 +23,7 @@
//! //!
//! #[derive(Debug, MultipartForm)] //! #[derive(Debug, MultipartForm)]
//! struct UploadForm { //! struct UploadForm {
//! // Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
//! #[multipart(limit = "100MB")] //! #[multipart(limit = "100MB")]
//! file: TempFile, //! file: TempFile,
//! json: MpJson<Metadata>, //! json: MpJson<Metadata>,
@ -38,10 +39,15 @@
//! //!
//! #[actix_web::main] //! #[actix_web::main]
//! async fn main() -> std::io::Result<()> { //! async fn main() -> std::io::Result<()> {
//! HttpServer::new(move || App::new().service(post_video)) //! HttpServer::new(move || {
//! .bind(("127.0.0.1", 8080))? //! App::new()
//! .run() //! .service(post_video)
//! .await //! // Also increase the global total limit to 100MiB.
//! .app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
//! })
//! .bind(("127.0.0.1", 8080))?
//! .run()
//! .await
//! } //! }
//! ``` //! ```
//! //!

View File

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

View File

@ -24,7 +24,7 @@ allowed_external_types = [
actix = { version = ">=0.12, <0.14", default-features = false } actix = { version = ">=0.12, <0.14", default-features = false }
actix-codec = "0.5" actix-codec = "0.5"
actix-http = "3" actix-http = "3"
actix-web = { version = "4", default-features = false } actix-web = { version = "4", default-features = false, features = ["ws"] }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"

View File

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

View File

@ -17,7 +17,6 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [ features = [
"macros", "macros",
"openssl", "openssl",
@ -68,6 +67,7 @@ default = [
"http2", "http2",
"unicode", "unicode",
"compat", "compat",
"ws",
] ]
# Brotli algorithm content-encoding support # Brotli algorithm content-encoding support
@ -86,9 +86,12 @@ cookies = ["dep:cookie"]
# Secure & signed cookies # Secure & signed cookies
secure-cookies = ["cookies", "cookie/secure"] secure-cookies = ["cookies", "cookie/secure"]
# HTTP/2 support (including h2c). # HTTP/2 support (including h2c)
http2 = ["actix-http/http2"] http2 = ["actix-http/http2"]
# WebSocket support
ws = ["actix-http/ws"]
# TLS via OpenSSL # TLS via OpenSSL
openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
@ -132,7 +135,7 @@ actix-service = "2"
actix-tls = { version = "3.4", default-features = false, optional = true } actix-tls = { version = "3.4", default-features = false, optional = true }
actix-utils = "3" 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-router = { version = "0.5.3", default-features = false, features = ["http"] }
actix-web-codegen = { version = "4.3", optional = true, default-features = false } actix-web-codegen = { version = "4.3", optional = true, default-features = false }

View File

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

View File

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

View File

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

View File

@ -318,12 +318,33 @@ impl HttpResponseBuilder {
/// Set a streaming body and build the `HttpResponse`. /// Set a streaming body and build the `HttpResponse`.
/// ///
/// `HttpResponseBuilder` can not be used after this call. /// `HttpResponseBuilder` can not be used after this call.
///
/// If `Content-Type` is not set, then it is automatically set to `application/octet-stream`.
///
/// If `Content-Length` is set, then [`no_chunking()`](Self::no_chunking) is automatically called.
#[inline] #[inline]
pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse
where where
S: Stream<Item = Result<Bytes, E>> + 'static, S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<BoxError> + 'static, E: Into<BoxError> + 'static,
{ {
// 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)) 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(( .insert_header((
header::CONTENT_TYPE, header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"), header::HeaderValue::from_static("application/json"),
@ -624,6 +624,7 @@ mod tests {
.set_payload(Bytes::from_static(&[0u8; 1000])) .set_payload(Bytes::from_static(&[0u8; 1000]))
.to_http_parts(); .to_http_parts();
req.head_mut().headers_mut().remove(header::CONTENT_LENGTH);
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true) let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true)
.limit(100) .limit(100)
.await; .await;

View File

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

View File

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

View File

@ -16,7 +16,6 @@ license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [ features = [
"cookies", "cookies",
"openssl", "openssl",
@ -109,7 +108,7 @@ cfg-if = "1"
derive_more = { version = "2", features = ["display", "error", "from"] } derive_more = { version = "2", features = ["display", "error", "from"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.26" h2 = "0.3.27"
http = "0.2.7" http = "0.2.7"
itoa = "1" itoa = "1"
log = "0.4" log = "0.4"

View File

@ -13,6 +13,8 @@ fmt:
[private] [private]
downgrade-for-msrv: downgrade-for-msrv:
cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0 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=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=idna_adapter --precise=1.2.0 # next ver: 1.82.0
cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0 cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
@ -50,8 +52,7 @@ clippy:
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }} cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
# Run Clippy over workspace using MSRV. # Run Clippy over workspace using MSRV.
clippy-msrv: clippy-msrv: downgrade-for-msrv
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
@just toolchain={{ msrv_rustup }} clippy @just toolchain={{ msrv_rustup }} clippy
# Test workspace code. # 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)" 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 workspace using MSRV.
test-msrv: test-msrv: downgrade-for-msrv
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
@just toolchain={{ msrv_rustup }} test @just toolchain={{ msrv_rustup }} test
# Test workspace docs. # 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 # developed on macOS and probably doesn't work on Linux yet due to minor
# differences in flags on sed # differences in flags on sed

View File

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

25
scripts/publish Executable file
View File

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