Merge branch 'main' into 2866

This commit is contained in:
Yuki Okushi 2026-02-03 20:39:19 +09:00 committed by GitHub
commit faa80379bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 1606 additions and 1139 deletions

43
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,43 @@
A-files:
- changed-files:
- any-glob-to-any-file: actix-files/**
A-http:
- changed-files:
- any-glob-to-any-file: actix-http/**
A-http-test:
- changed-files:
- any-glob-to-any-file: actix-http-test/**
A-multipart:
- changed-files:
- any-glob-to-any-file: actix-multipart/**
A-multipart-derive:
- changed-files:
- any-glob-to-any-file: actix-multipart-derive/**
A-router:
- changed-files:
- any-glob-to-any-file: actix-router/**
A-test:
- changed-files:
- any-glob-to-any-file: actix-test/**
A-web:
- changed-files:
- any-glob-to-any-file: actix-web/**
A-web-actors:
- changed-files:
- any-glob-to-any-file: actix-web-actors/**
A-web-codegen:
- changed-files:
- any-glob-to-any-file: actix-web-codegen/**
A-awc:
- changed-files:
- any-glob-to-any-file: awc/**

View File

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

View File

@ -2,7 +2,7 @@ name: CI (post-merge)
on: on:
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -28,7 +28,7 @@ jobs:
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install nasm - name: Install nasm
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@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@0e09747a63ae497bf945b3dcaf38fef0050d0109 # v2.62.0 uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -71,7 +71,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Free Disk Space - name: Free Disk Space
run: ./scripts/free-disk-space.sh run: ./scripts/free-disk-space.sh
@ -80,10 +80,10 @@ jobs:
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
- name: Install just, cargo-hack - name: Install just, cargo-hack
uses: taiki-e/install-action@0e09747a63ae497bf945b3dcaf38fef0050d0109 # v2.62.0 uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18
with: with:
tool: just,cargo-hack tool: just,cargo-hack

View File

@ -6,7 +6,7 @@ on:
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -39,7 +39,7 @@ jobs:
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install nasm - name: Install nasm
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
@ -59,12 +59,12 @@ jobs:
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@0e09747a63ae497bf945b3dcaf38fef0050d0109 # v2.62.0 uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -85,14 +85,18 @@ jobs:
- name: CI cache clean - name: CI cache clean
run: cargo-ci-cache-clean run: cargo-ci-cache-clean
- name: deny check
if: matrix.version.name == 'stable' && matrix.target.os == 'ubuntu-latest'
uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15
io-uring: io-uring:
name: io-uring tests name: io-uring tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
@ -105,15 +109,15 @@ jobs:
name: doc tests name: doc tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
- name: Install just - name: Install just
uses: taiki-e/install-action@0e09747a63ae497bf945b3dcaf38fef0050d0109 # v2.62.0 uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18
with: with:
tool: just tool: just

View File

@ -2,7 +2,7 @@ name: Coverage
on: on:
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -15,16 +15,16 @@ jobs:
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
components: llvm-tools components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest - name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@0e09747a63ae497bf945b3dcaf38fef0050d0109 # v2.62.0 uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18
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@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with: with:
files: codecov.json files: codecov.json
fail_ci_if_error: true fail_ci_if_error: true

16
.github/workflows/labeler.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: Labeler
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
labeler:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1

View File

@ -15,10 +15,10 @@ jobs:
fmt: fmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
components: rustfmt components: rustfmt
@ -33,10 +33,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
components: clippy components: clippy
@ -52,10 +52,10 @@ jobs:
lint-docs: lint-docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
components: rust-docs components: rust-docs
@ -69,20 +69,20 @@ jobs:
if: false # rustdoc mismatch currently if: false # rustdoc mismatch currently
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }}) - name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
uses: actions-rust-lang/setup-rust-toolchain@2fcdc490d667999e01ddbbf0f2823181beef6b39 # v1.15.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just - name: Install just
uses: taiki-e/install-action@0e09747a63ae497bf945b3dcaf38fef0050d0109 # v2.62.0 uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18
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@b33c63d3b3c85540f4eba8a4f71a5cc0ce030855 # v2.3.0 uses: taiki-e/cache-cargo-install-action@34ce5120836e5f9f1508d8713d7fdea0e8facd6f # v3.0.1
with: with:
tool: cargo-check-external-types tool: cargo-check-external-types

29
.github/workflows/semver-labeler.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Semver Labeler
on:
workflow_run:
workflows: [CI]
types: [completed]
jobs:
semver-label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
env:
ACTIONS_STEP_DEBUG: true
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with:
toolchain: stable
- uses: JohnTitor/cargo-semver-checks@3b76737b550e48ad0bd5912e2757e80eee6294b0 # v0.2.1
with:
label-prefix: B-semver-
label-strategy: skip-if-human

1220
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.88"
[profile.dev] [profile.dev]
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.

View File

@ -2,6 +2,15 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
- `PathBufWrap` & `UriSegmentError` made public. [#3694]
[#3694]: https://github.com/actix/actix-web/pull/3694
## 0.6.9
- Correct `derive_more` dependency feature requirements.
## 0.6.8 ## 0.6.8
- Add `Files::with_permanent_redirect()` method. - Add `Files::with_permanent_redirect()` method.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.8" version = "0.6.9"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "Static file serving for Actix Web" description = "Static file serving for Actix Web"
keywords = ["actix", "http", "async", "futures"] keywords = ["actix", "http", "async", "futures"]
@ -24,7 +24,7 @@ actix-web = { version = "4", default-features = false }
bitflags = "2" bitflags = "2"
bytes = "1" bytes = "1"
derive_more = { version = "2", features = ["display", "error", "from"] } derive_more = { version = "2", features = ["deref", "deref_mut", "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"] }
http-range = "0.1.4" http-range = "0.1.4"
log = "0.4" log = "0.4"
@ -37,7 +37,7 @@ v_htmlescape = "0.15.5"
# experimental-io-uring # experimental-io-uring
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
tokio-uring = { version = "0.5", optional = true, features = ["bytes"] } tokio-uring = { version = "0.5", optional = true, features = ["bytes"] }
actix-server = { version = "2.4", optional = true } # ensure matching tokio-uring versions actix-server = { version = "2.4", optional = true } # ensure matching tokio-uring versions
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7" actix-rt = "2.7"

View File

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

View File

@ -21,6 +21,7 @@ impl ResponseError for FilesError {
} }
} }
/// Error which can occur with parsing/validating a request-uri path
#[derive(Debug, PartialEq, Eq, Display)] #[derive(Debug, PartialEq, Eq, Display)]
#[non_exhaustive] #[non_exhaustive]
pub enum UriSegmentError { pub enum UriSegmentError {

View File

@ -220,11 +220,11 @@ impl Files {
/// Sets the size threshold that determines file read mode (sync/async). /// 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 /// When a file is smaller than the threshold (bytes), the reader will use synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large /// (blocking) file reads. For larger files, it switches to async reads to avoid blocking the
/// files. /// main thread.
/// ///
/// Tweaking this value according to your expected usage may lead to signifiant performance /// Tweaking this value according to your expected usage may lead to significant performance
/// gains (or losses in other handlers, if `size` is too high). /// 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. /// When the `experimental-io-uring` crate feature is enabled, file reads are always async.

View File

@ -14,7 +14,7 @@
#![warn(missing_docs, missing_debug_implementations)] #![warn(missing_docs, missing_debug_implementations)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
use std::path::Path; use std::path::Path;
@ -37,13 +37,12 @@ mod range;
mod service; mod service;
pub use self::{ pub use self::{
chunked::ChunkedReadFile, directory::Directory, files::Files, named::NamedFile, chunked::ChunkedReadFile, directory::Directory, error::UriSegmentError, files::Files,
range::HttpRange, service::FilesService, named::NamedFile, path_buf::PathBufWrap, range::HttpRange, service::FilesService,
}; };
use self::{ use self::{
directory::{directory_listing, DirectoryRenderer}, directory::{directory_listing, DirectoryRenderer},
error::FilesError, error::FilesError,
path_buf::PathBufWrap,
}; };
type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>; type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>;

View File

@ -357,11 +357,11 @@ impl NamedFile {
/// Sets the size threshold that determines file read mode (sync/async). /// 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 /// When a file is smaller than the threshold (bytes), the reader will use synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large /// (blocking) file reads. For larger files, it switches to async reads to avoid blocking the
/// files. /// main thread.
/// ///
/// Tweaking this value according to your expected usage may lead to signifiant performance /// Tweaking this value according to your expected usage may lead to significant performance
/// gains (or losses in other handlers, if `size` is too high). /// 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. /// When the `experimental-io-uring` crate feature is enabled, file reads are always async.

View File

@ -8,8 +8,11 @@ use actix_web::{dev::Payload, FromRequest, HttpRequest};
use crate::error::UriSegmentError; use crate::error::UriSegmentError;
/// Secure Path Traversal Guard
///
/// This struct parses a request-uri [`PathBuf`](std::path::PathBuf)
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub(crate) struct PathBufWrap(PathBuf); pub struct PathBufWrap(PathBuf);
impl FromStr for PathBufWrap { impl FromStr for PathBufWrap {
type Err = UriSegmentError; type Err = UriSegmentError;
@ -20,6 +23,37 @@ impl FromStr for PathBufWrap {
} }
impl PathBufWrap { impl PathBufWrap {
/// Parse a safe path from the unprocessed tail of a supplied
/// [`HttpRequest`](actix_web::HttpRequest), given the choice of allowing hidden files to be
/// considered valid segments.
///
/// This uses [`HttpRequest::match_info`](actix_web::HttpRequest::match_info) and
/// [`Path::unprocessed`](actix_web::dev::Path::unprocessed), which returns the part of the
/// path not matched by route patterns. This is useful for mounted services (eg. `Files`),
/// where only the tail should be parsed.
///
/// Path traversal is guarded by this method.
#[inline]
pub fn parse_unprocessed_req(
req: &HttpRequest,
hidden_files: bool,
) -> Result<Self, UriSegmentError> {
Self::parse_path(req.match_info().unprocessed(), hidden_files)
}
/// Parse a safe path from the full request path of a supplied
/// [`HttpRequest`](actix_web::HttpRequest), given the choice of allowing hidden files to be
/// considered valid segments.
///
/// This uses [`HttpRequest::path`](actix_web::HttpRequest::path), and is more appropriate
/// for non-mounted handlers that want the entire request path.
///
/// Path traversal is guarded by this method.
#[inline]
pub fn parse_req_path(req: &HttpRequest, hidden_files: bool) -> Result<Self, UriSegmentError> {
Self::parse_path(req.path(), hidden_files)
}
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments. /// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
/// ///
/// Path traversal is guarded by this method. /// Path traversal is guarded by this method.
@ -91,6 +125,7 @@ impl FromRequest for PathBufWrap {
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
// Uses the unprocessed tail of the request path and disallows hidden files.
ready(req.match_info().unprocessed().parse()) ready(req.match_info().unprocessed().parse())
} }
} }

View File

@ -294,16 +294,11 @@ mod tests {
let res = HttpRange::parse(header, size); let res = HttpRange::parse(header, size);
if res.is_err() { if let Err(err) = res {
if expected.is_empty() { if expected.is_empty() {
continue; continue;
} else { } else {
panic!( panic!("parse({header}, {size}) returned error {err:?}");
"parse({}, {}) returned error {:?}",
header,
size,
res.unwrap_err()
);
} }
} }

View File

@ -2,7 +2,7 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.88.
## 3.2.0 ## 3.2.0

View File

@ -4,7 +4,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.2.0)](https://docs.rs/actix-http-test/3.2.0) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.2.0)](https://docs.rs/actix-http-test/3.2.0)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br> <br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.2.0/status.svg)](https://deps.rs/crate/actix-http-test/3.2.0) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.2.0/status.svg)](https://deps.rs/crate/actix-http-test/3.2.0)

View File

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

View File

@ -2,7 +2,17 @@
## Unreleased ## Unreleased
- Properly wake Payload receivers when feeding errors or EOF - Minimum supported Rust version (MSRV) is now 1.88.
- Fix truncated body ending without error when connection closed abnormally. [#3067]
[#3067]: https://github.com/actix/actix-web/pull/3067
## 3.11.2
- Properly wake Payload receivers when feeding errors or EOF.
- Add `ServiceConfigBuilder` type to facilitate future configuration extensions.
- Add a configuration option to allow/disallow half closed connections in HTTP/1. This defaults to allow, reverting the change made in 3.11.1.
- Shutdown connections when HTTP Responses are written without reading full Requests.
## 3.11.1 ## 3.11.1

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.11.1" version = "3.11.2"
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"]
@ -149,7 +149,7 @@ memchr = "2.4"
once_cell = "1.21" once_cell = "1.21"
rcgen = "0.13" rcgen = "0.13"
regex = "1.3" regex = "1.3"
rustls-pemfile = "2" rustls-pki-types = "1.13.1"
rustversion = "1" rustversion = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

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.1)](https://docs.rs/actix-http/3.11.1) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.11.2)](https://docs.rs/actix-http/3.11.2)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.88+-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.1/status.svg)](https://deps.rs/crate/actix-http/3.11.1) [![dependency status](https://deps.rs/crate/actix-http/3.11.2/status.svg)](https://deps.rs/crate/actix-http/3.11.2)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -45,25 +45,14 @@ async fn main() -> io::Result<()> {
fn rustls_config() -> rustls::ServerConfig { fn rustls_config() -> rustls::ServerConfig {
let rcgen::CertifiedKey { cert, key_pair } = let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
let cert_file = cert.pem(); let cert_chain = vec![cert.der().clone()];
let key_file = key_pair.serialize_pem(); let key_der = rustls_pki_types::PrivateKeyDer::Pkcs8(
rustls_pki_types::PrivatePkcs8KeyDer::from(key_pair.serialize_der()),
let cert_file = &mut io::BufReader::new(cert_file.as_bytes()); );
let key_file = &mut io::BufReader::new(key_file.as_bytes());
let cert_chain = rustls_pemfile::certs(cert_file)
.collect::<Result<Vec<_>, _>>()
.unwrap();
let mut keys = rustls_pemfile::pkcs8_private_keys(key_file)
.collect::<Result<Vec<_>, _>>()
.unwrap();
let mut config = rustls::ServerConfig::builder() let mut config = rustls::ServerConfig::builder()
.with_no_client_auth() .with_no_client_auth()
.with_single_cert( .with_single_cert(cert_chain, key_der)
cert_chain,
rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
)
.unwrap(); .unwrap();
const H1_ALPN: &[u8] = b"http/1.1"; const H1_ALPN: &[u8] = b"http/1.1";

View File

@ -82,29 +82,16 @@ impl Stream for Heartbeat {
} }
fn tls_config() -> rustls::ServerConfig { fn tls_config() -> rustls::ServerConfig {
use std::io::BufReader;
use rustls_pemfile::{certs, pkcs8_private_keys};
let rcgen::CertifiedKey { cert, key_pair } = let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
let cert_file = cert.pem(); let cert_chain = vec![cert.der().clone()];
let key_file = key_pair.serialize_pem(); let key_der = rustls_pki_types::PrivateKeyDer::Pkcs8(
rustls_pki_types::PrivatePkcs8KeyDer::from(key_pair.serialize_der()),
let cert_file = &mut BufReader::new(cert_file.as_bytes()); );
let key_file = &mut BufReader::new(key_file.as_bytes());
let cert_chain = certs(cert_file).collect::<Result<Vec<_>, _>>().unwrap();
let mut keys = pkcs8_private_keys(key_file)
.collect::<Result<Vec<_>, _>>()
.unwrap();
let mut config = rustls::ServerConfig::builder() let mut config = rustls::ServerConfig::builder()
.with_no_client_auth() .with_no_client_auth()
.with_single_cert( .with_single_cert(cert_chain, key_der)
cert_chain,
rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
)
.unwrap(); .unwrap();
config.alpn_protocols.push(b"http/1.1".to_vec()); config.alpn_protocols.push(b"http/1.1".to_vec());

View File

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

View File

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

View File

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

View File

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

View File

@ -176,11 +176,7 @@ impl Inner {
/// Register future waiting data from payload. /// Register future waiting data from payload.
/// Waker would be used in `Inner::wake` /// Waker would be used in `Inner::wake`
fn register(&mut self, cx: &Context<'_>) { fn register(&mut self, cx: &Context<'_>) {
if self if self.task.as_ref().is_none_or(|w| !cx.waker().will_wake(w)) {
.task
.as_ref()
.map_or(true, |w| !cx.waker().will_wake(w))
{
self.task = Some(cx.waker().clone()); self.task = Some(cx.waker().clone());
} }
} }
@ -191,7 +187,7 @@ impl Inner {
if self if self
.io_task .io_task
.as_ref() .as_ref()
.map_or(true, |w| !cx.waker().will_wake(w)) .is_none_or(|w| !cx.waker().will_wake(w))
{ {
self.io_task = Some(cx.waker().clone()); self.io_task = Some(cx.waker().clone());
} }

View File

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

View File

@ -275,6 +275,7 @@ impl TestSeqBuffer {
{ {
Self(Rc::new(RefCell::new(TestSeqInner { Self(Rc::new(RefCell::new(TestSeqInner {
read_buf: data.into(), read_buf: data.into(),
read_closed: false,
write_buf: BytesMut::new(), write_buf: BytesMut::new(),
err: None, err: None,
}))) })))
@ -293,36 +294,59 @@ impl TestSeqBuffer {
Ref::map(self.0.borrow(), |inner| &inner.write_buf) Ref::map(self.0.borrow(), |inner| &inner.write_buf)
} }
pub fn take_write_buf(&self) -> Bytes {
self.0.borrow_mut().write_buf.split().freeze()
}
pub fn err(&self) -> Ref<'_, Option<io::Error>> { pub fn err(&self) -> Ref<'_, Option<io::Error>> {
Ref::map(self.0.borrow(), |inner| &inner.err) Ref::map(self.0.borrow(), |inner| &inner.err)
} }
/// Add data to read buffer. /// Add data to read buffer.
///
/// # Panics
///
/// Panics if called after [`TestSeqBuffer::close_read`] has been called
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) { pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
self.0 let mut inner = self.0.borrow_mut();
.borrow_mut() if inner.read_closed {
.read_buf panic!("Tried to extend the read buffer after calling close_read");
.extend_from_slice(data.as_ref()) }
inner.read_buf.extend_from_slice(data.as_ref())
}
/// Closes the [`AsyncRead`]/[`Read`] part of this test buffer.
///
/// The current data in the buffer will still be returned by a call to read/poll_read, however,
/// after the buffer is empty, it will return `Ok(0)` to signify the EOF condition
pub fn close_read(&self) {
self.0.borrow_mut().read_closed = true;
} }
} }
pub struct TestSeqInner { pub struct TestSeqInner {
read_buf: BytesMut, read_buf: BytesMut,
read_closed: bool,
write_buf: BytesMut, write_buf: BytesMut,
err: Option<io::Error>, err: Option<io::Error>,
} }
impl io::Read for TestSeqBuffer { impl io::Read for TestSeqBuffer {
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> { fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
if self.0.borrow().read_buf.is_empty() { let mut inner = self.0.borrow_mut();
if self.0.borrow().err.is_some() {
Err(self.0.borrow_mut().err.take().unwrap()) if inner.read_buf.is_empty() {
if let Some(err) = inner.err.take() {
Err(err)
} else if inner.read_closed {
Ok(0)
} else { } else {
Err(io::Error::new(io::ErrorKind::WouldBlock, "")) Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
} }
} else { } else {
let size = std::cmp::min(self.0.borrow().read_buf.len(), dst.len()); let size = std::cmp::min(inner.read_buf.len(), dst.len());
let b = self.0.borrow_mut().read_buf.split_to(size); let b = inner.read_buf.split_to(size);
dst[..size].copy_from_slice(&b); dst[..size].copy_from_slice(&b);
Ok(size) Ok(size)
} }

View File

@ -4,7 +4,7 @@ extern crate tls_rustls_023 as rustls;
use std::{ use std::{
convert::Infallible, convert::Infallible,
io::{self, BufReader, Write}, io::{self, Write},
net::{SocketAddr, TcpStream as StdTcpStream}, net::{SocketAddr, TcpStream as StdTcpStream},
sync::Arc, sync::Arc,
task::Poll, task::Poll,
@ -27,7 +27,7 @@ use derive_more::{Display, Error};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use futures_util::stream::once; use futures_util::stream::once;
use rustls::{pki_types::ServerName, ServerConfig as RustlsServerConfig}; use rustls::{pki_types::ServerName, ServerConfig as RustlsServerConfig};
use rustls_pemfile::{certs, pkcs8_private_keys}; use rustls_pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
async fn load_body<S>(stream: S) -> Result<BytesMut, PayloadError> async fn load_body<S>(stream: S) -> Result<BytesMut, PayloadError>
where where
@ -51,34 +51,34 @@ where
Ok(buf) Ok(buf)
} }
fn tls_config() -> RustlsServerConfig { fn tls_config_with_alpn(protocols: &[&[u8]]) -> RustlsServerConfig {
let rcgen::CertifiedKey { cert, key_pair } = let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
let cert_file = cert.pem(); let cert_chain = vec![cert.der().clone()];
let key_file = key_pair.serialize_pem(); let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
let cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes());
let cert_chain = certs(cert_file).collect::<Result<Vec<_>, _>>().unwrap();
let mut keys = pkcs8_private_keys(key_file)
.collect::<Result<Vec<_>, _>>()
.unwrap();
let mut config = RustlsServerConfig::builder() let mut config = RustlsServerConfig::builder()
.with_no_client_auth() .with_no_client_auth()
.with_single_cert( .with_single_cert(cert_chain, key_der)
cert_chain,
rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
)
.unwrap(); .unwrap();
config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec()); config.alpn_protocols = protocols.iter().map(|proto| proto.to_vec()).collect();
config.alpn_protocols.push(H2_ALPN_PROTOCOL.to_vec());
config config
} }
fn tls_config() -> RustlsServerConfig {
tls_config_with_alpn(&[HTTP1_1_ALPN_PROTOCOL, H2_ALPN_PROTOCOL])
}
fn tls_config_h1() -> RustlsServerConfig {
tls_config_with_alpn(&[HTTP1_1_ALPN_PROTOCOL])
}
fn tls_config_h2() -> RustlsServerConfig {
tls_config_with_alpn(&[H2_ALPN_PROTOCOL])
}
pub fn get_negotiated_alpn_protocol( pub fn get_negotiated_alpn_protocol(
addr: SocketAddr, addr: SocketAddr,
client_alpn_protocol: &[u8], client_alpn_protocol: &[u8],
@ -109,7 +109,7 @@ async fn h1() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok())) .h1(|_| ok::<_, Error>(Response::ok()))
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h1())
}) })
.await; .await;
@ -123,7 +123,7 @@ async fn h2() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok())) .h2(|_| ok::<_, Error>(Response::ok()))
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -141,7 +141,7 @@ async fn h1_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_11); assert_eq!(req.version(), Version::HTTP_11);
ok::<_, Error>(Response::ok()) ok::<_, Error>(Response::ok())
}) })
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h1())
}) })
.await; .await;
@ -160,7 +160,7 @@ async fn h2_1() -> io::Result<()> {
ok::<_, Error>(Response::ok()) ok::<_, Error>(Response::ok())
}) })
.rustls_0_23_with_config( .rustls_0_23_with_config(
tls_config(), tls_config_h2(),
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)), TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
) )
}) })
@ -180,7 +180,7 @@ async fn h2_body1() -> io::Result<()> {
let body = load_body(req.take_payload()).await?; let body = load_body(req.take_payload()).await?;
Ok::<_, Error>(Response::ok().set_body(body)) Ok::<_, Error>(Response::ok().set_body(body))
}) })
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -206,7 +206,7 @@ async fn h2_content_length() {
]; ];
ok::<_, Infallible>(Response::new(statuses[indx])) ok::<_, Infallible>(Response::new(statuses[indx]))
}) })
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -278,7 +278,7 @@ async fn h2_headers() {
} }
ok::<_, Infallible>(config.body(data.clone())) ok::<_, Infallible>(config.body(data.clone()))
}) })
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -317,7 +317,7 @@ async fn h2_body2() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -334,7 +334,7 @@ async fn h2_head_empty() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -360,7 +360,7 @@ async fn h2_head_binary() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -385,7 +385,7 @@ async fn h2_head_binary2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -411,7 +411,7 @@ async fn h2_body_length() {
Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -435,7 +435,7 @@ async fn h2_body_chunked_explicit() {
.body(BodyStream::new(body)), .body(BodyStream::new(body)),
) )
}) })
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -464,7 +464,7 @@ async fn h2_response_http_error_handling() {
) )
})) }))
})) }))
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -494,7 +494,7 @@ async fn h2_service_error() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| err::<Response<BoxBody>, _>(BadRequest)) .h2(|_| err::<Response<BoxBody>, _>(BadRequest))
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h2())
}) })
.await; .await;
@ -511,7 +511,7 @@ async fn h1_service_error() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h1(|_| err::<Response<BoxBody>, _>(BadRequest)) .h1(|_| err::<Response<BoxBody>, _>(BadRequest))
.rustls_0_23(tls_config()) .rustls_0_23(tls_config_h1())
}) })
.await; .await;
@ -530,7 +530,7 @@ const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom";
#[actix_rt::test] #[actix_rt::test]
async fn alpn_h1() -> io::Result<()> { async fn alpn_h1() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
let mut config = tls_config(); let mut config = tls_config_h1();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok())) .h1(|_| ok::<_, Error>(Response::ok()))
@ -552,7 +552,7 @@ async fn alpn_h1() -> io::Result<()> {
#[actix_rt::test] #[actix_rt::test]
async fn alpn_h2() -> io::Result<()> { async fn alpn_h2() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
let mut config = tls_config(); let mut config = tls_config_h2();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build() HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok())) .h2(|_| ok::<_, Error>(Response::ok()))

View File

@ -443,6 +443,60 @@ async fn content_length() {
srv.stop().await; srv.stop().await;
} }
#[actix_rt::test]
async fn content_length_truncated() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut srv = test_server(|| {
HttpService::build()
.h1(|mut req: Request| async move {
let expected_length: usize = req.uri().path()[1..].parse().unwrap();
let mut payload = req.take_payload();
let mut length = 0;
let mut seen_error = false;
while let Some(chunk) = payload.next().await {
match chunk {
Ok(b) => length += b.len(),
Err(_) => {
seen_error = true;
break;
}
}
}
if seen_error {
return Result::<_, Infallible>::Ok(Response::bad_request());
}
assert_eq!(length, expected_length, "length must match when no error");
Result::<_, Infallible>::Ok(Response::ok())
})
.tcp()
})
.await;
let addr = srv.addr();
let mut buf = [0; 12];
let mut conn = TcpStream::connect(&addr).await.unwrap();
conn.write_all(b"POST /10000 HTTP/1.1\r\nContent-Length: 10000\r\n\r\ndata_truncated")
.await
.unwrap();
conn.shutdown().await.unwrap();
conn.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"HTTP/1.1 400");
let mut conn = TcpStream::connect(&addr).await.unwrap();
conn.write_all(b"POST /4 HTTP/1.1\r\nContent-Length: 4\r\n\r\ndata")
.await
.unwrap();
conn.shutdown().await.unwrap();
conn.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"HTTP/1.1 200");
srv.stop().await;
}
#[actix_rt::test] #[actix_rt::test]
async fn h1_headers() { async fn h1_headers() {
let data = STR.repeat(10); let data = STR.repeat(10);

View File

@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 0.7.0 ## 0.7.0
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.72.

View File

@ -6,7 +6,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-multipart-derive?label=latest)](https://crates.io/crates/actix-multipart-derive) [![crates.io](https://img.shields.io/crates/v/actix-multipart-derive?label=latest)](https://crates.io/crates/actix-multipart-derive)
[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.7.0)](https://docs.rs/actix-multipart-derive/0.7.0) [![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.7.0)](https://docs.rs/actix-multipart-derive/0.7.0)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart-derive.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart-derive.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.7.0/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.7.0) [![dependency status](https://deps.rs/crate/actix-multipart-derive/0.7.0/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.7.0)

View File

@ -4,7 +4,7 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::disallowed_names)] // false positives in some macro expansions #![allow(clippy::disallowed_names)] // false positives in some macro expansions
use std::collections::HashSet; use std::collections::HashSet;
@ -16,19 +16,14 @@ use proc_macro2::Ident;
use quote::quote; use quote::quote;
use syn::{parse_macro_input, Type}; use syn::{parse_macro_input, Type};
#[derive(FromMeta)] #[derive(Default, FromMeta)]
enum DuplicateField { enum DuplicateField {
#[default]
Ignore, Ignore,
Deny, Deny,
Replace, Replace,
} }
impl Default for DuplicateField {
fn default() -> Self {
Self::Ignore
}
}
#[derive(FromDeriveInput, Default)] #[derive(FromDeriveInput, Default)]
#[darling(attributes(multipart), default)] #[darling(attributes(multipart), default)]
struct MultipartFormAttrs { struct MultipartFormAttrs {

View File

@ -1,10 +1,10 @@
error: Could not parse size limit `2 bytes`: couldn't parse "bytes" into a known SI unit, couldn't parse unit of "bytes" error: Could not parse size limit `2 bytes`: couldn't parse "bytes" into a known SI unit, Failed to parse unit "byt..."
--> tests/trybuild/size-limit-parse-fail.rs:6:5 --> tests/trybuild/size-limit-parse-fail.rs:6:5
| |
6 | description: Text<String>, 6 | description: Text<String>,
| ^^^^^^^^^^^ | ^^^^^^^^^^^
error: Could not parse size limit `2 megabytes`: couldn't parse "megabytes" into a known SI unit, couldn't parse unit of "megabytes" error: Could not parse size limit `2 megabytes`: couldn't parse "megabytes" into a known SI unit, Failed to parse unit "meg..."
--> tests/trybuild/size-limit-parse-fail.rs:12:5 --> tests/trybuild/size-limit-parse-fail.rs:12:5
| |
12 | description: Text<String>, 12 | description: Text<String>,

View File

@ -2,7 +2,10 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.75. - Add `MultipartForm` support for `Option<Vec<T>>` fields. [#3577]
- Minimum supported Rust version (MSRV) is now 1.88.
[#3577]: https://github.com/actix/actix-web/pull/3577
## 0.7.2 ## 0.7.2

View File

@ -4,7 +4,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.7.2)](https://docs.rs/actix-multipart/0.7.2) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.7.2)](https://docs.rs/actix-multipart/0.7.2)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.7.2/status.svg)](https://deps.rs/crate/actix-multipart/0.7.2) [![dependency status](https://deps.rs/crate/actix-multipart/0.7.2/status.svg)](https://deps.rs/crate/actix-multipart/0.7.2)
@ -84,4 +84,4 @@ curl -v --request POST \
<!-- cargo-rdme end --> <!-- cargo-rdme end -->
[More available in the examples repo &rarr;](https://github.com/actix/examples/tree/master/forms/multipart) [More available in the examples repo &rarr;](https://github.com/actix/examples/tree/main/forms/multipart)

View File

@ -187,6 +187,45 @@ where
} }
} }
impl<'t, T> FieldGroupReader<'t> for Option<Vec<T>>
where
T: FieldReader<'t>,
{
type Future = LocalBoxFuture<'t, Result<(), MultipartError>>;
fn handle_field(
req: &'t HttpRequest,
field: Field,
limits: &'t mut Limits,
state: &'t mut State,
_duplicate_field: DuplicateField,
) -> Self::Future {
let field_name = field.name().unwrap().to_string();
Box::pin(async move {
let vec = state
.entry(field_name)
.or_insert_with(|| Box::<Vec<T>>::default())
.downcast_mut::<Vec<T>>()
.unwrap();
let item = T::read_field(req, field, limits).await?;
vec.push(item);
Ok(())
})
}
fn from_state(name: &str, state: &'t mut State) -> Result<Self, MultipartError> {
if let Some(boxed_vec) = state.remove(name) {
let vec = *boxed_vec.downcast::<Vec<T>>().unwrap();
Ok(Some(vec))
} else {
Ok(None)
}
}
}
/// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor. /// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor.
/// ///
/// You should use the [`macro@MultipartForm`] macro to derive this for your struct. /// You should use the [`macro@MultipartForm`] macro to derive this for your struct.
@ -506,6 +545,40 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
} }
/// Test `Option<Vec>` fields.
#[derive(MultipartForm)]
struct TestOptionVec {
list1: Option<Vec<Text<String>>>,
list2: Option<Vec<Text<String>>>,
}
async fn test_option_vec_route(form: MultipartForm<TestOptionVec>) -> impl Responder {
let form = form.into_inner();
let strings = form
.list1
.unwrap()
.into_iter()
.map(|s| s.into_inner())
.collect::<Vec<_>>();
assert_eq!(strings, vec!["value1", "value2", "value3"]);
assert!(form.list2.is_none());
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_option_vec() {
let srv =
actix_test::start(|| App::new().route("/", web::post().to(test_option_vec_route)));
let mut form = multipart::Form::default();
form.add_text("list1", "value1");
form.add_text("list1", "value2");
form.add_text("list1", "value3");
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
}
/// Test the `rename` field attribute. /// Test the `rename` field attribute.
#[derive(MultipartForm)] #[derive(MultipartForm)]
struct TestFieldRenaming { struct TestFieldRenaming {

View File

@ -64,7 +64,7 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
// This allows us to use the actix_multipart_derive within this crate's tests // This allows us to use the actix_multipart_derive within this crate's tests
#[cfg(test)] #[cfg(test)]

View File

@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 0.5.3 ## 0.5.3
- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size. - Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size.

View File

@ -4,7 +4,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-router?label=latest)](https://crates.io/crates/actix-router) [![crates.io](https://img.shields.io/crates/v/actix-router?label=latest)](https://crates.io/crates/actix-router)
[![Documentation](https://docs.rs/actix-router/badge.svg?version=0.5.3)](https://docs.rs/actix-router/0.5.3) [![Documentation](https://docs.rs/actix-router/badge.svg?version=0.5.3)](https://docs.rs/actix-router/0.5.3)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-router.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-router.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-router/0.5.3/status.svg)](https://deps.rs/crate/actix-router/0.5.3) [![dependency status](https://deps.rs/crate/actix-router/0.5.3/status.svg)](https://deps.rs/crate/actix-router/0.5.3)

View File

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

View File

@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 0.1.5 ## 0.1.5
- Add `TestServerConfig::listen_address()` method. - Add `TestServerConfig::listen_address()` method.

View File

@ -4,7 +4,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-test?label=latest)](https://crates.io/crates/actix-test) [![crates.io](https://img.shields.io/crates/v/actix-test?label=latest)](https://crates.io/crates/actix-test)
[![Documentation](https://docs.rs/actix-test/badge.svg?version=0.1.5)](https://docs.rs/actix-test/0.1.5) [![Documentation](https://docs.rs/actix-test/badge.svg?version=0.1.5)](https://docs.rs/actix-test/0.1.5)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-test.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-test.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-test/0.1.5/status.svg)](https://deps.rs/crate/actix-test/0.1.5) [![dependency status](https://deps.rs/crate/actix-test/0.1.5/status.svg)](https://deps.rs/crate/actix-test/0.1.5)

View File

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

View File

@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 4.3.1 <!-- v4.3.1+deprecated --> ## 4.3.1 <!-- v4.3.1+deprecated -->
- Reduce memory usage by `take`-ing (rather than `split`-ing) the encoded buffer when yielding bytes in the response stream. - Reduce memory usage by `take`-ing (rather than `split`-ing) the encoded buffer when yielding bytes in the response stream.

View File

@ -8,7 +8,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.3.1)](https://docs.rs/actix-web-actors/4.3.1) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.3.1)](https://docs.rs/actix-web-actors/4.3.1)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-web-actors.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br /> <br />
![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red.svg) ![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red.svg)

View File

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

View File

@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 4.3.0 ## 4.3.0
- Add `#[scope]` macro. - Add `#[scope]` macro.

View File

@ -6,7 +6,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.3.0)](https://docs.rs/actix-web-codegen/4.3.0) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.3.0)](https://docs.rs/actix-web-codegen/4.3.0)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-web-codegen.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/4.3.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.3.0) [![dependency status](https://deps.rs/crate/actix-web-codegen/4.3.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.3.0)

View File

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

View File

@ -13,14 +13,14 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String>
| required by a bound introduced by this call | required by a bound introduced by this call
| |
= help: the following other types implement trait `HttpServiceFactory`: = help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B) (A, B)
(A, B, C) (A, B, C)
(A, B, C, D) (A, B, C, D)
(A, B, C, D, E)
(A, B, C, D, E, F)
(A, B, C, D, E, F, G)
(A, B, C, D, E, F, G, H)
(A, B, C, D, E, F, G, H, I)
and $N others and $N others
note: required by a bound in `App::<T>::service` note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs --> $WORKSPACE/actix-web/src/app.rs

View File

@ -13,14 +13,14 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String>
| required by a bound introduced by this call | required by a bound introduced by this call
| |
= help: the following other types implement trait `HttpServiceFactory`: = help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B) (A, B)
(A, B, C) (A, B, C)
(A, B, C, D) (A, B, C, D)
(A, B, C, D, E)
(A, B, C, D, E, F)
(A, B, C, D, E, F, G)
(A, B, C, D, E, F, G, H)
(A, B, C, D, E, F, G, H, I)
and $N others and $N others
note: required by a bound in `App::<T>::service` note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs --> $WORKSPACE/actix-web/src/app.rs

View File

@ -15,14 +15,14 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String>
| required by a bound introduced by this call | required by a bound introduced by this call
| |
= help: the following other types implement trait `HttpServiceFactory`: = help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B) (A, B)
(A, B, C) (A, B, C)
(A, B, C, D) (A, B, C, D)
(A, B, C, D, E)
(A, B, C, D, E, F)
(A, B, C, D, E, F, G)
(A, B, C, D, E, F, G, H)
(A, B, C, D, E, F, G, H, I)
and $N others and $N others
note: required by a bound in `App::<T>::service` note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs --> $WORKSPACE/actix-web/src/app.rs

View File

@ -29,14 +29,14 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String>
| required by a bound introduced by this call | required by a bound introduced by this call
| |
= help: the following other types implement trait `HttpServiceFactory`: = help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B) (A, B)
(A, B, C) (A, B, C)
(A, B, C, D) (A, B, C, D)
(A, B, C, D, E)
(A, B, C, D, E, F)
(A, B, C, D, E, F, G)
(A, B, C, D, E, F, G, H)
(A, B, C, D, E, F, G, H, I)
and $N others and $N others
note: required by a bound in `App::<T>::service` note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs --> $WORKSPACE/actix-web/src/app.rs

View File

@ -15,14 +15,14 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String>
| required by a bound introduced by this call | required by a bound introduced by this call
| |
= help: the following other types implement trait `HttpServiceFactory`: = help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B) (A, B)
(A, B, C) (A, B, C)
(A, B, C, D) (A, B, C, D)
(A, B, C, D, E)
(A, B, C, D, E, F)
(A, B, C, D, E, F, G)
(A, B, C, D, E, F, G, H)
(A, B, C, D, E, F, G, H, I)
and $N others and $N others
note: required by a bound in `App::<T>::service` note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs --> $WORKSPACE/actix-web/src/app.rs

View File

@ -2,9 +2,21 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
- Add `HttpRequest::url_for_map` and `HttpRequest::url_for_iter` methods for named URL parameters. [#3895]
[#3895]: https://github.com/actix/actix-web/pull/3895
## 4.12.1
- Correct `actix-http` dependency requirement.
## 4.12.0
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now sets `Content-Type` to `application/octet-stream` if `Content-Type` does not exist. - `actix_web::response::builder::HttpResponseBuilder::streaming()` now 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. - `actix_web::response::builder::HttpResponseBuilder::streaming()` now calls `actix_web::response::builder::HttpResponseBuilder::no_chunking()` and returns `SizedStream` if `Content-Length` is set by user.
- Add `ws` crate feature (on-by-default) which forwards to `actix-http` and guards some of its `ResponseError` impls. - Add `ws` crate feature (on-by-default) which forwards to `actix-http` and guards some of its `ResponseError` impls.
- Add public export for `EitherExtractError` in `error` module.
## 4.11.0 ## 4.11.0

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.11.0" version = "4.12.1"
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@ -134,7 +134,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 = "3.11" actix-http = "3.11.2"
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 }
@ -179,7 +179,7 @@ flate2 = "1.0.13"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] } futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
rand = "0.9" rand = "0.9"
rcgen = "0.13" rcgen = "0.13"
rustls-pemfile = "2" rustls-pki-types = "1.13.1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.55" } tls-openssl = { package = "openssl", version = "0.10.55" }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
use std::{ use std::{
cell::{Ref, RefCell, RefMut}, cell::{Ref, RefCell, RefMut},
fmt, net, collections::HashMap,
fmt,
hash::{BuildHasher, Hash},
net,
rc::Rc, rc::Rc,
str, str,
}; };
@ -242,6 +245,76 @@ impl HttpRequest {
self.resource_map().url_for(self, name, elements) self.resource_map().url_for(self, name, elements)
} }
/// Generates URL for a named resource using a map of dynamic segment values.
///
/// This substitutes URL parameters by name from `elements`, including parameters from parent
/// scopes.
///
/// # Examples
/// ```
/// # use std::collections::HashMap;
/// # use actix_web::{web, App, HttpRequest, HttpResponse};
/// fn index(req: HttpRequest) -> HttpResponse {
/// let mut params = HashMap::new();
/// params.insert("one", "1");
/// params.insert("two", "2");
/// let url = req.url_for_map("foo", &params); // <- generate URL for "foo" resource
/// HttpResponse::Ok().into()
/// }
///
/// let app = App::new()
/// .service(web::resource("/test/{one}/{two}")
/// .name("foo") // <- set resource name so it can be used in `url_for_map`
/// .route(web::get().to(|| HttpResponse::Ok()))
/// );
/// ```
pub fn url_for_map<K, V, S>(
&self,
name: &str,
elements: &HashMap<K, V, S>,
) -> Result<url::Url, UrlGenerationError>
where
K: std::borrow::Borrow<str> + Eq + Hash,
V: AsRef<str>,
S: BuildHasher,
{
self.resource_map().url_for_map(self, name, elements)
}
/// Generates URL for a named resource using an iterator of key-value pairs.
///
/// This is a convenience wrapper around [`HttpRequest::url_for_map`].
///
/// Note: passing a borrowed map (e.g. `&HashMap<String, String>`) directly does not satisfy the
/// trait bounds because the iterator yields `(&String, &String)`. Prefer `url_for_map` for
/// borrowed maps, or map entries to `&str`:
///
/// ```
/// # use std::collections::HashMap;
/// # use actix_web::{web, App, HttpRequest, HttpResponse};
/// fn index(req: HttpRequest) -> HttpResponse {
/// let mut params = HashMap::new();
/// params.insert("one".to_string(), "1".to_string());
/// params.insert("two".to_string(), "2".to_string());
///
/// let iter = params.iter().map(|(k, v)| (k.as_str(), v.as_str()));
/// let url = req.url_for_iter("foo", iter);
/// HttpResponse::Ok().into()
/// }
/// ```
pub fn url_for_iter<K, V, I>(
&self,
name: &str,
elements: I,
) -> Result<url::Url, UrlGenerationError>
where
I: IntoIterator<Item = (K, V)>,
K: std::borrow::Borrow<str> + Eq + Hash,
V: AsRef<str>,
{
self.resource_map().url_for_iter(self, name, elements)
}
/// Generate URL for named resource /// Generate URL for named resource
/// ///
/// This method is similar to `HttpRequest::url_for()` but it can be used /// This method is similar to `HttpRequest::url_for()` but it can be used
@ -550,6 +623,8 @@ impl HttpRequestPool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap;
use bytes::Bytes; use bytes::Bytes;
use super::*; use super::*;
@ -638,6 +713,59 @@ mod tests {
); );
} }
#[test]
fn test_url_for_map() {
let mut res = ResourceDef::new("/user/{name}.{ext}");
res.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut res, None);
let req = TestRequest::default()
.insert_header((header::HOST, "www.actix.rs"))
.rmap(rmap)
.to_http_request();
let mut params = HashMap::new();
params.insert("name", "test");
params.insert("ext", "html");
let url = req.url_for_map("index", &params);
assert_eq!(
url.ok().unwrap().as_str(),
"http://www.actix.rs/user/test.html"
);
params.remove("ext");
assert_eq!(
req.url_for_map("index", &params),
Err(UrlGenerationError::NotEnoughElements)
);
}
#[test]
fn test_url_for_iter() {
let mut res = ResourceDef::new("/user/{name}.{ext}");
res.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut res, None);
let req = TestRequest::default()
.insert_header((header::HOST, "www.actix.rs"))
.rmap(rmap)
.to_http_request();
let url = req.url_for_iter("index", [("ext", "html"), ("name", "test")]);
assert_eq!(
url.ok().unwrap().as_str(),
"http://www.actix.rs/user/test.html"
);
let url = req.url_for_iter("index", [("name", "test")]);
assert_eq!(url, Err(UrlGenerationError::NotEnoughElements));
}
#[test] #[test]
fn test_url_for_static() { fn test_url_for_static() {
let mut rdef = ResourceDef::new("/index.html"); let mut rdef = ResourceDef::new("/index.html");

View File

@ -11,7 +11,7 @@ use futures_core::Stream;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
body::{BodyStream, BoxBody, MessageBody}, body::{BodyStream, BoxBody, MessageBody, SizedStream},
dev::Extensions, dev::Extensions,
error::{Error, JsonPayloadError}, error::{Error, JsonPayloadError},
http::{ http::{
@ -335,17 +335,18 @@ impl HttpResponseBuilder {
} }
} }
if let Some(parts) = self.inner() { let content_length = self
if let Some(length) = parts.headers.get(header::CONTENT_LENGTH) { .inner()
if let Ok(length) = length.to_str() { .and_then(|parts| parts.headers.get(header::CONTENT_LENGTH))
if let Ok(length) = length.parse::<u64>() { .and_then(|value| value.to_str().ok())
self.no_chunking(length); .and_then(|value| value.parse::<u64>().ok());
}
}
}
}
self.body(BodyStream::new(stream)) if let Some(len) = content_length {
self.no_chunking(len);
self.body(SizedStream::new(len, stream))
} else {
self.body(BodyStream::new(stream))
}
} }
/// Set a JSON body and build the `HttpResponse`. /// Set a JSON body and build the `HttpResponse`.

View File

@ -1,7 +1,9 @@
use std::{ use std::{
borrow::Cow, borrow::{Borrow, Cow},
cell::RefCell, cell::RefCell,
collections::HashMap,
fmt::Write as _, fmt::Write as _,
hash::{BuildHasher, Hash},
rc::{Rc, Weak}, rc::{Rc, Weak},
}; };
@ -140,6 +142,56 @@ impl ResourceMap {
}) })
.ok_or(UrlGenerationError::NotEnoughElements)?; .ok_or(UrlGenerationError::NotEnoughElements)?;
self.url_from_path(req, path)
}
/// Generate URL for named resource using map of dynamic segment values.
///
/// Check [`HttpRequest::url_for_map`] for detailed information.
pub fn url_for_map<K, V, S>(
&self,
req: &HttpRequest,
name: &str,
elements: &HashMap<K, V, S>,
) -> Result<Url, UrlGenerationError>
where
K: Borrow<str> + Eq + Hash,
V: AsRef<str>,
S: BuildHasher,
{
let path = self
.named
.get(name)
.ok_or(UrlGenerationError::ResourceNotFound)?
.root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
node.pattern
.resource_path_from_map(&mut acc, elements)
.then_some(acc)
})
.ok_or(UrlGenerationError::NotEnoughElements)?;
self.url_from_path(req, path)
}
/// Generate URL for named resource using an iterator of key-value pairs.
///
/// Check [`HttpRequest::url_for_iter`] for detailed information.
pub fn url_for_iter<K, V, I>(
&self,
req: &HttpRequest,
name: &str,
elements: I,
) -> Result<Url, UrlGenerationError>
where
I: IntoIterator<Item = (K, V)>,
K: Borrow<str> + Eq + Hash,
V: AsRef<str>,
{
let elements = elements.into_iter().collect::<FoldHashMap<K, V>>();
self.url_for_map(req, name, &elements)
}
fn url_from_path(&self, req: &HttpRequest, path: String) -> Result<Url, UrlGenerationError> {
let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') { let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') {
// build full URL from connection info parts and resource path // build full URL from connection info parts and resource path
let conn = req.connection_info(); let conn = req.connection_info();

View File

@ -31,6 +31,7 @@ struct Config {
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_request_timeout: Duration, client_request_timeout: Duration,
client_disconnect_timeout: Duration, client_disconnect_timeout: Duration,
h1_allow_half_closed: bool,
#[allow(dead_code)] // only dead when no TLS features are enabled #[allow(dead_code)] // only dead when no TLS features are enabled
tls_handshake_timeout: Option<Duration>, tls_handshake_timeout: Option<Duration>,
} }
@ -116,6 +117,7 @@ where
keep_alive: KeepAlive::default(), keep_alive: KeepAlive::default(),
client_request_timeout: Duration::from_secs(5), client_request_timeout: Duration::from_secs(5),
client_disconnect_timeout: Duration::from_secs(1), client_disconnect_timeout: Duration::from_secs(1),
h1_allow_half_closed: true,
tls_handshake_timeout: None, tls_handshake_timeout: None,
})), })),
backlog: 1024, backlog: 1024,
@ -257,6 +259,18 @@ where
self.client_disconnect_timeout(Duration::from_millis(dur)) self.client_disconnect_timeout(Duration::from_millis(dur))
} }
/// Sets whether HTTP/1 connections should support half-closures.
///
/// Clients can choose to shutdown their writer-side of the connection after completing their
/// request and while waiting for the server response. Setting this to `false` will cause the
/// server to abort the connection handling as soon as it detects an EOF from the client.
///
/// The default behavior is to allow, i.e. `true`
pub fn h1_allow_half_closed(self, allow: bool) -> Self {
self.config.lock().unwrap().h1_allow_half_closed = allow;
self
}
/// Sets function that will be called once before each connection is handled. /// Sets function that will be called once before each connection is handled.
/// ///
/// It will receive a `&std::any::Any`, which contains underlying connection type and an /// It will receive a `&std::any::Any`, which contains underlying connection type and an
@ -558,6 +572,7 @@ where
.keep_alive(cfg.keep_alive) .keep_alive(cfg.keep_alive)
.client_request_timeout(cfg.client_request_timeout) .client_request_timeout(cfg.client_request_timeout)
.client_disconnect_timeout(cfg.client_disconnect_timeout) .client_disconnect_timeout(cfg.client_disconnect_timeout)
.h1_allow_half_closed(cfg.h1_allow_half_closed)
.local_addr(addr); .local_addr(addr);
if let Some(handler) = on_connect_fn.clone() { if let Some(handler) = on_connect_fn.clone() {
@ -602,6 +617,7 @@ where
.keep_alive(cfg.keep_alive) .keep_alive(cfg.keep_alive)
.client_request_timeout(cfg.client_request_timeout) .client_request_timeout(cfg.client_request_timeout)
.client_disconnect_timeout(cfg.client_disconnect_timeout) .client_disconnect_timeout(cfg.client_disconnect_timeout)
.h1_allow_half_closed(cfg.h1_allow_half_closed)
.local_addr(addr); .local_addr(addr);
if let Some(handler) = on_connect_fn.clone() { if let Some(handler) = on_connect_fn.clone() {
@ -677,6 +693,7 @@ where
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -728,6 +745,7 @@ where
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -794,6 +812,7 @@ where
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -860,6 +879,7 @@ where
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -927,6 +947,7 @@ where
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.client_disconnect_timeout(c.client_disconnect_timeout) .client_disconnect_timeout(c.client_disconnect_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.local_addr(addr); .local_addr(addr);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
@ -995,6 +1016,7 @@ where
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.client_disconnect_timeout(c.client_disconnect_timeout) .client_disconnect_timeout(c.client_disconnect_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.finish(map_config(fac, move |_| config.clone())), .finish(map_config(fac, move |_| config.clone())),
) )
}, },
@ -1036,6 +1058,7 @@ where
let mut svc = HttpService::build() let mut svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout) .client_request_timeout(c.client_request_timeout)
.h1_allow_half_closed(c.h1_allow_half_closed)
.client_disconnect_timeout(c.client_disconnect_timeout); .client_disconnect_timeout(c.client_disconnect_timeout);
if let Some(handler) = on_connect_fn.clone() { if let Some(handler) = on_connect_fn.clone() {

View File

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

View File

@ -688,30 +688,20 @@ async fn test_brotli_encoding_large_openssl() {
#[cfg(feature = "rustls-0_23")] #[cfg(feature = "rustls-0_23")]
mod plus_rustls { mod plus_rustls {
use std::io::BufReader;
use rustls::{pki_types::PrivateKeyDer, ServerConfig as RustlsServerConfig}; use rustls::{pki_types::PrivateKeyDer, ServerConfig as RustlsServerConfig};
use rustls_pemfile::{certs, pkcs8_private_keys}; use rustls_pki_types::PrivatePkcs8KeyDer;
use super::*; use super::*;
fn tls_config() -> RustlsServerConfig { fn tls_config() -> RustlsServerConfig {
let rcgen::CertifiedKey { cert, key_pair } = let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
let cert_file = cert.pem(); let cert_chain = vec![cert.der().clone()];
let key_file = key_pair.serialize_pem(); let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
let cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes());
let cert_chain = certs(cert_file).collect::<Result<Vec<_>, _>>().unwrap();
let mut keys = pkcs8_private_keys(key_file)
.collect::<Result<Vec<_>, _>>()
.unwrap();
RustlsServerConfig::builder() RustlsServerConfig::builder()
.with_no_client_auth() .with_no_client_auth()
.with_single_cert(cert_chain, PrivateKeyDer::Pkcs8(keys.remove(0))) .with_single_cert(cert_chain, key_der)
.unwrap() .unwrap()
} }

View File

@ -2,6 +2,12 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 3.8.1
- Fix a bug where `GO_AWAY` errors did not stop connections from returning to the pool.
## 3.8.0 ## 3.8.0
- Add `hickory-dns` crate feature (off-by-default). - Add `hickory-dns` crate feature (off-by-default).

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.8.0" version = "3.8.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP and WebSocket client library" description = "Async HTTP and WebSocket client library"
keywords = ["actix", "http", "framework", "async", "web"] keywords = ["actix", "http", "framework", "async", "web"]
@ -149,7 +149,7 @@ flate2 = "1.0.13"
futures-util = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false }
static_assertions = "1.1" static_assertions = "1.1"
rcgen = "0.13" rcgen = "0.13"
rustls-pemfile = "2" rustls-pki-types = "1.13.1"
tokio = { version = "1.38.2", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.38.2", features = ["rt-multi-thread", "macros"] }
zstd = "0.13" zstd = "0.13"
tls-rustls-0_23 = { package = "rustls", version = "0.23" } # add rustls 0.23 with default features to make aws_lc_rs work in tests tls-rustls-0_23 = { package = "rustls", version = "0.23" } # add rustls 0.23 with default features to make aws_lc_rs work in tests

View File

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

View File

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

View File

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

View File

@ -309,10 +309,7 @@ impl ClientRequest {
/// Freeze request builder and construct `FrozenClientRequest`, /// Freeze request builder and construct `FrozenClientRequest`,
/// which could be used for sending same request multiple times. /// which could be used for sending same request multiple times.
pub fn freeze(self) -> Result<FrozenClientRequest, FreezeRequestError> { pub fn freeze(self) -> Result<FrozenClientRequest, FreezeRequestError> {
let slf = match self.prep_for_sending() { let slf = self.prep_for_sending()?;
Ok(slf) => slf,
Err(err) => return Err(err.into()),
};
let request = FrozenClientRequest { let request = FrozenClientRequest {
head: Rc::new(slf.head), head: Rc::new(slf.head),

View File

@ -2,12 +2,9 @@
extern crate tls_rustls_0_23 as rustls; extern crate tls_rustls_0_23 as rustls;
use std::{ use std::sync::{
io::BufReader, atomic::{AtomicUsize, Ordering},
sync::{ Arc,
atomic::{AtomicUsize, Ordering},
Arc,
},
}; };
use actix_http::HttpService; use actix_http::HttpService;
@ -16,29 +13,18 @@ use actix_service::{fn_service, map_config, ServiceFactoryExt};
use actix_tls::connect::rustls_0_23::webpki_roots_cert_store; use actix_tls::connect::rustls_0_23::webpki_roots_cert_store;
use actix_utils::future::ok; use actix_utils::future::ok;
use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse};
use rustls::{ use rustls::{pki_types::ServerName, ClientConfig, ServerConfig};
pki_types::{CertificateDer, PrivateKeyDer, ServerName}, use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
ClientConfig, ServerConfig,
};
use rustls_pemfile::{certs, pkcs8_private_keys};
fn tls_config() -> ServerConfig { fn tls_config() -> ServerConfig {
let rcgen::CertifiedKey { cert, key_pair } = let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
let cert_file = cert.pem(); let cert_chain = vec![cert.der().clone()];
let key_file = key_pair.serialize_pem(); let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
let cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes());
let cert_chain = certs(cert_file).collect::<Result<Vec<_>, _>>().unwrap();
let mut keys = pkcs8_private_keys(key_file)
.collect::<Result<Vec<_>, _>>()
.unwrap();
ServerConfig::builder() ServerConfig::builder()
.with_no_client_auth() .with_no_client_auth()
.with_single_cert(cert_chain, PrivateKeyDer::Pkcs8(keys.remove(0))) .with_single_cert(cert_chain, key_der)
.unwrap() .unwrap()
} }

45
deny.toml Normal file
View File

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

View File

@ -12,14 +12,7 @@ fmt:
# Downgrade dependencies necessary to run MSRV checks/tests. # Downgrade dependencies necessary to run MSRV checks/tests.
[private] [private]
downgrade-for-msrv: downgrade-for-msrv:
cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0 # no downgrades currently needed
cargo {{ toolchain }} update -p=rayon --precise=1.10.0 # next ver: 1.80.0
cargo {{ toolchain }} update -p=rayon-core --precise=1.12.1 # next ver: 1.80.0
cargo {{ toolchain }} update -p=half --precise=2.4.1 # next ver: 1.81.0
cargo {{ toolchain }} update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0
cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
cargo {{ toolchain }} update -p=zerofrom --precise=0.1.5 # next ver: 1.81.0
cargo {{ toolchain }} update -p=time --precise=0.3.41 # next ver: 1.81.0
msrv := ``` msrv := ```
cargo metadata --format-version=1 \ cargo metadata --format-version=1 \
@ -91,7 +84,7 @@ test-coverage-lcov: test-coverage
# Document crates in workspace. # Document crates in workspace.
doc *args: && doc-set-workspace-crates doc *args: && doc-set-workspace-crates
rm -f "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js" rm -f "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js"
RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --workspace {{ all_crate_features }} {{ args }} RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --no-deps --workspace {{ all_crate_features }} {{ args }}
[private] [private]
doc-set-workspace-crates: doc-set-workspace-crates: