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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ name: Coverage
on:
push:
branches: [master]
branches: [main]
permissions:
contents: read
@ -15,16 +15,16 @@ jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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:
toolchain: nightly
components: llvm-tools
- 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:
tool: just,cargo-llvm-cov,cargo-nextest
@ -32,7 +32,7 @@ jobs:
run: just test-coverage-codecov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
files: codecov.json
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:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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:
toolchain: nightly
components: rustfmt
@ -33,10 +33,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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:
components: clippy
@ -52,10 +52,10 @@ jobs:
lint-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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:
toolchain: nightly
components: rust-docs
@ -69,20 +69,20 @@ jobs:
if: false # rustdoc mismatch currently
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just
uses: taiki-e/install-action@0e09747a63ae497bf945b3dcaf38fef0050d0109 # v2.62.0
uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18
with:
tool: just
- 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:
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"
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.75"
rust-version = "1.88"
[profile.dev]
# 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
- 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
- Add `Files::with_permanent_redirect()` method.

View File

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

View File

@ -3,11 +3,11 @@
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.8)](https://docs.rs/actix-files/0.6.8)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
[![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.88+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg)
<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)
[![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)]
#[non_exhaustive]
pub enum UriSegmentError {

View File

@ -220,11 +220,11 @@ impl Files {
/// Sets the size threshold that determines file read mode (sync/async).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
/// When a file is smaller than the threshold (bytes), the reader will use synchronous
/// (blocking) file reads. For larger files, it switches to async reads to avoid blocking the
/// 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).
///
/// 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)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::path::Path;
@ -37,13 +37,12 @@ mod range;
mod service;
pub use self::{
chunked::ChunkedReadFile, directory::Directory, files::Files, named::NamedFile,
range::HttpRange, service::FilesService,
chunked::ChunkedReadFile, directory::Directory, error::UriSegmentError, files::Files,
named::NamedFile, path_buf::PathBufWrap, range::HttpRange, service::FilesService,
};
use self::{
directory::{directory_listing, DirectoryRenderer},
error::FilesError,
path_buf::PathBufWrap,
};
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).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
/// When a file is smaller than the threshold (bytes), the reader will use synchronous
/// (blocking) file reads. For larger files, it switches to async reads to avoid blocking the
/// 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).
///
/// 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;
/// Secure Path Traversal Guard
///
/// This struct parses a request-uri [`PathBuf`](std::path::PathBuf)
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct PathBufWrap(PathBuf);
pub struct PathBufWrap(PathBuf);
impl FromStr for PathBufWrap {
type Err = UriSegmentError;
@ -20,6 +23,37 @@ impl FromStr for 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.
///
/// Path traversal is guarded by this method.
@ -91,6 +125,7 @@ impl FromRequest for PathBufWrap {
type Future = Ready<Result<Self, Self::Error>>;
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())
}
}

View File

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

View File

@ -2,7 +2,7 @@
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.72.
- Minimum supported Rust version (MSRV) is now 1.88.
## 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)
[![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)
<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)

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "3.11.1"
version = "3.11.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "HTTP types and services for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"]
@ -149,7 +149,7 @@ memchr = "2.4"
once_cell = "1.21"
rcgen = "0.13"
regex = "1.3"
rustls-pemfile = "2"
rustls-pki-types = "1.13.1"
rustversion = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"

View File

@ -5,11 +5,11 @@
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.11.1)](https://docs.rs/actix-http/3.11.1)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
[![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.88+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-http/3.11.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)
[![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 {
let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
let cert_file = cert.pem();
let key_file = key_pair.serialize_pem();
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 cert_chain = vec![cert.der().clone()];
let key_der = rustls_pki_types::PrivateKeyDer::Pkcs8(
rustls_pki_types::PrivatePkcs8KeyDer::from(key_pair.serialize_der()),
);
let mut config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(
cert_chain,
rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
)
.with_single_cert(cert_chain, key_der)
.unwrap();
const H1_ALPN: &[u8] = b"http/1.1";

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -443,6 +443,60 @@ async fn content_length() {
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]
async fn h1_headers() {
let data = STR.repeat(10);

View File

@ -2,6 +2,8 @@
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 0.7.0
- 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)
[![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)
<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)

View File

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

View File

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

View File

@ -4,7 +4,7 @@
[![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)
![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)
<br />
[![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 -->
[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.
///
/// 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);
}
/// 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.
#[derive(MultipartForm)]
struct TestFieldRenaming {

View File

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

View File

@ -2,6 +2,8 @@
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 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.

View File

@ -4,7 +4,7 @@
[![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)
![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)
<br />
[![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_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]
mod de;
mod path;

View File

@ -2,6 +2,8 @@
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 0.1.5
- 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)
[![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)
<br />
[![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_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;

View File

@ -2,6 +2,8 @@
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 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.

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)
[![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)
<br />
![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_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]
mod context;
pub mod ws;

View File

@ -2,6 +2,8 @@
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88.
## 4.3.0
- 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)
[![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)
<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)

View File

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

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
|
= help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B)
(A, B, C)
(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
note: required by a bound in `App::<T>::service`
--> $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
|
= help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B)
(A, B, C)
(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
note: required by a bound in `App::<T>::service`
--> $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
|
= help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B)
(A, B, C)
(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
note: required by a bound in `App::<T>::service`
--> $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
|
= help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B)
(A, B, C)
(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
note: required by a bound in `App::<T>::service`
--> $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
|
= help: the following other types implement trait `HttpServiceFactory`:
Resource<T>
actix_web::Scope<T>
Vec<T>
Redirect
(A,)
(A, B)
(A, B, C)
(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
note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs

View File

@ -2,9 +2,21 @@
## 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 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 public export for `EitherExtractError` in `error` module.
## 4.11.0

View File

@ -1,6 +1,6 @@
[package]
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"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
keywords = ["actix", "http", "web", "framework", "async"]
@ -134,7 +134,7 @@ actix-service = "2"
actix-tls = { version = "3.4", default-features = false, optional = true }
actix-utils = "3"
actix-http = "3.11"
actix-http = "3.11.2"
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
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"] }
rand = "0.9"
rcgen = "0.13"
rustls-pemfile = "2"
rustls-pki-types = "1.13.1"
serde = { version = "1", features = ["derive"] }
static_assertions = "1"
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
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

View File

@ -8,10 +8,10 @@
<!-- prettier-ignore-start -->
[![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)
![MSRV](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
[![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.88+-ab6000.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 />
[![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)
@ -44,7 +44,7 @@
- [Website & User Guide](https://actix.rs)
- [Examples Repository](https://github.com/actix/examples)
- [API Documentation](https://docs.rs/actix-web)
- [API Documentation (master branch)](https://actix.rs/actix-web/actix_web)
- [API Documentation (mainranch)](https://actix.rs/actix-web/actix_web)
## Example
@ -78,23 +78,23 @@ async fn main() -> std::io::Result<()> {
### More Examples
- [Hello World](https://github.com/actix/examples/tree/master/basics/hello-world)
- [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics)
- [Application State](https://github.com/actix/examples/tree/master/basics/state)
- [JSON Handling](https://github.com/actix/examples/tree/master/json/json)
- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart)
- [MongoDB Integration](https://github.com/actix/examples/tree/master/databases/mongodb)
- [Diesel Integration](https://github.com/actix/examples/tree/master/databases/diesel)
- [SQLite Integration](https://github.com/actix/examples/tree/master/databases/sqlite)
- [Postgres Integration](https://github.com/actix/examples/tree/master/databases/postgres)
- [Tera Templates](https://github.com/actix/examples/tree/master/templating/tera)
- [Askama Templates](https://github.com/actix/examples/tree/master/templating/askama)
- [HTTPS using Rustls](https://github.com/actix/examples/tree/master/https-tls/rustls)
- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/https-tls/openssl)
- [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets)
- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat)
- [Hello World](https://github.com/actix/examples/tree/main/basics/hello-world)
- [Basic Setup](https://github.com/actix/examples/tree/main/basics/basics)
- [Application State](https://github.com/actix/examples/tree/main/basics/state)
- [JSON Handling](https://github.com/actix/examples/tree/main/json/json)
- [Multipart Streams](https://github.com/actix/examples/tree/main/forms/multipart)
- [MongoDB Integration](https://github.com/actix/examples/tree/main/databases/mongodb)
- [Diesel Integration](https://github.com/actix/examples/tree/main/databases/diesel)
- [SQLite Integration](https://github.com/actix/examples/tree/main/databases/sqlite)
- [Postgres Integration](https://github.com/actix/examples/tree/main/databases/postgres)
- [Tera Templates](https://github.com/actix/examples/tree/main/templating/tera)
- [Askama Templates](https://github.com/actix/examples/tree/main/templating/askama)
- [HTTPS using Rustls](https://github.com/actix/examples/tree/main/https-tls/rustls)
- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/main/https-tls/openssl)
- [Simple WebSocket](https://github.com/actix/examples/tree/main/websockets)
- [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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
use std::{
cell::{Ref, RefCell, RefMut},
fmt, net,
collections::HashMap,
fmt,
hash::{BuildHasher, Hash},
net,
rc::Rc,
str,
};
@ -242,6 +245,76 @@ impl HttpRequest {
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
///
/// This method is similar to `HttpRequest::url_for()` but it can be used
@ -550,6 +623,8 @@ impl HttpRequestPool {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use bytes::Bytes;
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]
fn test_url_for_static() {
let mut rdef = ResourceDef::new("/index.html");

View File

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

View File

@ -1,7 +1,9 @@
use std::{
borrow::Cow,
borrow::{Borrow, Cow},
cell::RefCell,
collections::HashMap,
fmt::Write as _,
hash::{BuildHasher, Hash},
rc::{Rc, Weak},
};
@ -140,6 +142,56 @@ impl ResourceMap {
})
.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('/') {
// build full URL from connection info parts and resource path
let conn = req.connection_info();

View File

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

View File

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

View File

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

View File

@ -2,6 +2,12 @@
## 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
- Add `hickory-dns` crate feature (off-by-default).

View File

@ -1,6 +1,6 @@
[package]
name = "awc"
version = "3.8.0"
version = "3.8.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP and WebSocket client library"
keywords = ["actix", "http", "framework", "async", "web"]
@ -149,7 +149,7 @@ flate2 = "1.0.13"
futures-util = { version = "0.3.17", default-features = false }
static_assertions = "1.1"
rcgen = "0.13"
rustls-pemfile = "2"
rustls-pki-types = "1.13.1"
tokio = { version = "1.38.2", features = ["rt-multi-thread", "macros"] }
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

View File

@ -5,16 +5,16 @@
<!-- prettier-ignore-start -->
[![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)
[![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)
<!-- prettier-ignore-end -->
## Examples
[Example project using TLS-enabled client →](https://github.com/actix/examples/tree/master/https-tls/awc-https)
[Example project using TLS-enabled client →](https://github.com/actix/examples/tree/main/https-tls/awc-https)
Basic usage:

View File

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

View File

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

View File

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

View File

@ -2,12 +2,9 @@
extern crate tls_rustls_0_23 as rustls;
use std::{
io::BufReader,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
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_utils::future::ok;
use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse};
use rustls::{
pki_types::{CertificateDer, PrivateKeyDer, ServerName},
ClientConfig, ServerConfig,
};
use rustls_pemfile::{certs, pkcs8_private_keys};
use rustls::{pki_types::ServerName, ClientConfig, ServerConfig};
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
fn tls_config() -> ServerConfig {
let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
let cert_file = cert.pem();
let key_file = key_pair.serialize_pem();
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 cert_chain = vec![cert.der().clone()];
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(cert_chain, PrivateKeyDer::Pkcs8(keys.remove(0)))
.with_single_cert(cert_chain, key_der)
.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.
[private]
downgrade-for-msrv:
cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0
cargo {{ toolchain }} update -p=rayon --precise=1.10.0 # next ver: 1.80.0
cargo {{ toolchain }} update -p=rayon-core --precise=1.12.1 # next ver: 1.80.0
cargo {{ toolchain }} update -p=half --precise=2.4.1 # next ver: 1.81.0
cargo {{ toolchain }} update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0
cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
cargo {{ toolchain }} update -p=zerofrom --precise=0.1.5 # next ver: 1.81.0
cargo {{ toolchain }} update -p=time --precise=0.3.41 # next ver: 1.81.0
# no downgrades currently needed
msrv := ```
cargo metadata --format-version=1 \
@ -91,7 +84,7 @@ test-coverage-lcov: test-coverage
# Document crates in workspace.
doc *args: && doc-set-workspace-crates
rm -f "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js"
RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --workspace {{ all_crate_features }} {{ args }}
RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --no-deps --workspace {{ all_crate_features }} {{ args }}
[private]
doc-set-workspace-crates: