Compare commits

..

No commits in common. "master" and "multipart-v0.6.1" have entirely different histories.

251 changed files with 3121 additions and 11613 deletions

14
.cargo/config.toml Normal file
View File

@ -0,0 +1,14 @@
[alias]
lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo"
# lib checking
ci-check-min = "hack --workspace check --no-default-features"
ci-check-default = "hack --workspace check"
ci-check-default-tests = "check --workspace --tests"
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,experimental-io-uring check"
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
# testing
ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture"
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"

View File

@ -1,7 +0,0 @@
disallowed-names = [
"e", # no single letter error bindings
]
disallowed-methods = [
"std::cell::RefCell::default()",
"std::rc::Rc::default()",
]

View File

@ -1,3 +0,0 @@
version: "0.2"
words:
- actix

View File

@ -1,10 +1,12 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: weekly
- package-ecosystem: github-actions
directory: /
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
interval: "monthly"
open-pull-requests-limit: 10

View File

@ -5,7 +5,7 @@ on:
branches: [master]
permissions:
contents: read
contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install Rust
run: |

View File

@ -28,64 +28,81 @@ jobs:
runs-on: ${{ matrix.target.os }}
steps:
- uses: actions/checkout@v4
- name: Install nasm
if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.2
- uses: actions/checkout@v3
- name: Install OpenSSL
if: matrix.target.os == 'windows-latest'
shell: bash
run: choco install openssl -y --forcex64 --no-progress
- name: Set OpenSSL dir in env
if: matrix.target.os == 'windows-latest'
run: |
set -e
choco install openssl --version=1.1.1.2100 -y --no-progress
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL-Win64' | Out-File -FilePath $env:GITHUB_ENV -Append
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.39
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: check minimal
run: just check-min
run: cargo ci-check-min
- name: check default
run: just check-default
run: cargo ci-check-default
- name: tests
timeout-minutes: 60
run: just test
run: |
cargo test --lib --tests -p=actix-router --all-features
cargo test --lib --tests -p=actix-http --all-features
cargo test --lib --tests -p=actix-web --features=rustls-0_20,rustls-0_21,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
cargo test --lib --tests -p=actix-web-codegen --all-features
cargo test --lib --tests -p=awc --all-features
cargo test --lib --tests -p=actix-http-test --all-features
cargo test --lib --tests -p=actix-test --all-features
cargo test --lib --tests -p=actix-files
cargo test --lib --tests -p=actix-multipart --all-features
cargo test --lib --tests -p=actix-web-actors --all-features
- name: CI cache clean
run: cargo-ci-cache-clean
- name: Clear the cargo caches
run: |
cargo install cargo-cache --version 0.8.3 --no-default-features --features ci-autoclean
cargo-cache
ci_feature_powerset_check:
name: Verify Feature Combinations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Free Disk Space
run: ./scripts/free-disk-space.sh
- name: Setup mold linker
uses: rui314/setup-mold@v1
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install just, cargo-hack
uses: taiki-e/install-action@v2.49.39
with:
tool: just,cargo-hack
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: Check feature combinations
run: just check-feature-combinations
- name: check feature combinations
run: cargo ci-check-all-feature-powerset
- name: check feature combinations
run: cargo ci-check-all-feature-powerset-linux
nextest:
name: nextest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Test with cargo-nextest
run: cargo nextest run

View File

@ -16,13 +16,7 @@ concurrency:
cancel-in-progress: true
jobs:
read_msrv:
name: Read MSRV
uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0
build_and_test:
needs: read_msrv
strategy:
fail-fast: false
matrix:
@ -32,69 +26,72 @@ jobs:
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
version:
- { name: msrv, version: "${{ needs.read_msrv.outputs.msrv }}" }
- { name: msrv, version: 1.68.0 }
- { name: stable, version: stable }
name: ${{ matrix.target.name }} / ${{ matrix.version.name }}
runs-on: ${{ matrix.target.os }}
steps:
- uses: actions/checkout@v4
- name: Install nasm
if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.2
- uses: actions/checkout@v3
- name: Install OpenSSL
if: matrix.target.os == 'windows-latest'
shell: bash
run: choco install openssl -y --forcex64 --no-progress
- name: Set OpenSSL dir in env
if: matrix.target.os == 'windows-latest'
run: |
set -e
choco install openssl --version=1.1.1.2100 -y --no-progress
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Setup mold linker
if: matrix.target.os == 'ubuntu-latest'
uses: rui314/setup-mold@v1
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL-Win64' | Out-File -FilePath $env:GITHUB_ENV -Append
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.39
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: workaround MSRV issues
if: matrix.version.name == 'msrv'
run: just downgrade-for-msrv
if: matrix.version.name != 'stable'
run: |
cargo update -p=clap --precise=4.3.24
cargo update -p=clap_lex --precise=0.5.0
- name: check minimal
run: just check-min
run: cargo ci-check-min
- name: check default
run: just check-default
run: cargo ci-check-default
- name: tests
timeout-minutes: 60
run: just test
run: |
cargo test --lib --tests -p=actix-router --all-features
cargo test --lib --tests -p=actix-http --all-features
cargo test --lib --tests -p=actix-web --features=rustls-0_20,rustls-0_21,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
cargo test --lib --tests -p=actix-web-codegen --all-features
cargo test --lib --tests -p=awc --all-features
cargo test --lib --tests -p=actix-http-test --all-features
cargo test --lib --tests -p=actix-test --all-features
cargo test --lib --tests -p=actix-files
cargo test --lib --tests -p=actix-multipart --all-features
cargo test --lib --tests -p=actix-web-actors --all-features
- name: CI cache clean
run: cargo-ci-cache-clean
- name: Clear the cargo caches
run: |
cargo install cargo-cache --version 0.8.3 --no-default-features --features ci-autoclean
cargo-cache
io-uring:
name: io-uring tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: nightly
uses: actions-rust-lang/setup-rust-toolchain@v1
with: { toolchain: nightly }
- name: tests (io-uring)
timeout-minutes: 60
@ -105,17 +102,12 @@ jobs:
name: doc tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: nightly
- name: Install just
uses: taiki-e/install-action@v2.49.39
with:
tool: just
uses: actions-rust-lang/setup-rust-toolchain@v1
with: { toolchain: nightly }
- name: doc tests
run: just test-docs
run: cargo ci-doctest
timeout-minutes: 60

75
.github/workflows/clippy-fmt.yml vendored Normal file
View File

@ -0,0 +1,75 @@
name: Lint
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
components: rustfmt
- run: cargo fmt --all -- --check
clippy:
permissions:
checks: write # to add clippy checks to PR diffs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: { components: clippy }
- uses: giraffate/clippy-action@v1
with:
reporter: 'github-pr-check'
github_token: ${{ secrets.GITHUB_TOKEN }}
clippy_flags: --workspace --all-features --tests --examples --bins -- -Dclippy::todo -Aunknown_lints
lint-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: { components: rust-docs }
- name: Check for broken intra-doc links
env: { RUSTDOCFLAGS: "-D warnings" }
run: cargo doc --no-deps --all-features --workspace
public-api-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.base_ref }}
- uses: actions/checkout@v3
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: { toolchain: nightly-2023-08-25 }
- uses: taiki-e/cache-cargo-install-action@v1
with: { tool: cargo-public-api }
- name: generate API diff
run: |
for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do
cargo public-api --manifest-path "$f" diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }}
done

View File

@ -15,26 +15,22 @@ jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
with:
toolchain: nightly
components: llvm-tools
components: llvm-tools-preview
- name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@v2.49.39
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@v2.13.4
with:
tool: just,cargo-llvm-cov,cargo-nextest
tool: cargo-llvm-cov
- name: Generate code coverage
run: just test-coverage-codecov
run: cargo llvm-cov --workspace --all-features --codecov --output-path codecov.json
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.0
uses: codecov/codecov-action@v3.1.4
with:
files: codecov.json
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,90 +0,0 @@
name: Lint
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: nightly
components: rustfmt
- name: Check with Rustfmt
run: cargo fmt --all -- --check
clippy:
permissions:
contents: read
checks: write # to add clippy checks to PR diffs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
components: clippy
- name: Check with Clippy
uses: giraffate/clippy-action@v1.0.1
with:
reporter: github-pr-check
github_token: ${{ secrets.GITHUB_TOKEN }}
clippy_flags: >-
--workspace --all-features --tests --examples --bins --
-A unknown_lints -D clippy::todo -D clippy::dbg_macro
lint-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: nightly
components: rust-docs
- name: Check for broken intra-doc links
env:
RUSTDOCFLAGS: -D warnings
run: cargo +nightly doc --no-deps --workspace --all-features
check-external-types:
if: false # rustdoc mismatch currently
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just
uses: taiki-e/install-action@v2.49.39
with:
tool: just
- name: Install cargo-check-external-types
uses: taiki-e/cache-cargo-install-action@v2.1.1
with:
tool: cargo-check-external-types
- name: check external types
run: just check-external-types-all +${{ vars.RUST_VERSION_EXTERNAL_TYPES }}

38
.github/workflows/upload-doc.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Upload Documentation
on:
push:
branches: [master]
permissions:
contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
permissions:
contents: write # to push changes in repo (jamesives/github-pages-deploy-action)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@nightly
- name: Build Docs
run: cargo +nightly doc --no-deps --workspace --all-features
env:
RUSTDOCFLAGS: --cfg=docsrs
- name: Tweak HTML
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4.4.3
with:
folder: target/doc
single-commit: true

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
Cargo.lock
target/
guide/build/
/gh-pages

View File

@ -1,5 +1,5 @@
overrides:
- files: "*.md"
- files: '*.md'
options:
printWidth: 9999
proseWrap: never

3970
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,11 +15,9 @@ members = [
]
[workspace.package]
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.68"
[profile.dev]
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
@ -51,11 +49,3 @@ awc = { path = "awc" }
# actix-utils = { path = "../actix-net/actix-utils" }
# actix-tls = { path = "../actix-net/actix-tls" }
# actix-server = { path = "../actix-net/actix-server" }
[workspace.lints.rust]
rust_2018_idioms = { level = "deny" }
future_incompatible = { level = "deny" }
nonstandard_style = { level = "deny" }
[workspace.lints.clippy]
# clone_on_ref_ptr = { level = "deny" }

View File

@ -2,20 +2,6 @@
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.6.6
- Update `tokio-uring` dependency to `0.4`.
- Minimum supported Rust version (MSRV) is now 1.72.
## 0.6.5
- Fix handling of special characters in filenames.
## 0.6.4
- Fix handling of newlines in filenames.
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
## 0.6.3

View File

@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.6.6"
version = "0.6.3"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
@ -13,14 +13,9 @@ categories = ["asynchronous", "web-programming::http-server"]
license = "MIT OR Apache-2.0"
edition = "2021"
[package.metadata.cargo_check_external_types]
allowed_external_types = [
"actix_http::*",
"actix_service::*",
"actix_web::*",
"http::*",
"mime::*",
]
[lib]
name = "actix_files"
path = "src/lib.rs"
[features]
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
@ -33,7 +28,7 @@ actix-web = { version = "4", default-features = false }
bitflags = "2"
bytes = "1"
derive_more = { version = "2", features = ["display", "error", "from"] }
derive_more = "0.99.5"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http-range = "0.1.4"
log = "0.4"
@ -45,15 +40,11 @@ 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
tokio-uring = { version = "0.4", optional = true, features = ["bytes"] }
actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions
[dev-dependencies]
actix-rt = "2.7"
actix-test = "0.1"
actix-web = "4"
env_logger = "0.11"
tempfile = "3.2"
[lints]
workspace = true

View File

@ -1,32 +1,18 @@
# `actix-files`
# actix-files
<!-- prettier-ignore-start -->
> Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.6)](https://docs.rs/actix-files/0.6.6)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.3)](https://docs.rs/actix-files/0.6.3)
![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.6/status.svg)](https://deps.rs/crate/actix-files/0.6.6)
[![dependency status](https://deps.rs/crate/actix-files/0.6.3/status.svg)](https://deps.rs/crate/actix-files/0.6.3)
[![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)
<!-- prettier-ignore-end -->
## Documentation & Resources
<!-- cargo-rdme start -->
Static file serving for Actix Web.
Provides a non-blocking service for serving static files from disk.
## Examples
```rust
use actix_web::App;
use actix_files::Files;
let app = App::new()
.service(Files::new("/static", ".").prefer_utf8(true));
```
<!-- cargo-rdme end -->
- [API Documentation](https://docs.rs/actix-files)
- [Example Project](https://github.com/actix/examples/tree/master/basics/static-files)
- Minimum Supported Rust Version (MSRV): 1.68

View File

@ -1,33 +0,0 @@
use actix_files::Files;
use actix_web::{get, guard, middleware, App, HttpServer, Responder};
const EXAMPLES_DIR: &str = concat![env!("CARGO_MANIFEST_DIR"), "/examples"];
#[get("/")]
async fn index() -> impl Responder {
"Hello world!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
log::info!("starting HTTP server at http://localhost:8080");
HttpServer::new(|| {
App::new()
.service(index)
.service(
Files::new("/assets", EXAMPLES_DIR)
.show_files_listing()
.guard(guard::Header("show-listing", "?1")),
)
.service(Files::new("/assets", EXAMPLES_DIR))
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
})
.bind(("127.0.0.1", 8080))?
.workers(2)
.run()
.await
}

View File

@ -6,11 +6,11 @@ use derive_more::Display;
pub enum FilesError {
/// Path is not a directory.
#[allow(dead_code)]
#[display("path is not a directory. Unable to serve static files")]
#[display(fmt = "path is not a directory. Unable to serve static files")]
IsNotDirectory,
/// Cannot render directory.
#[display("unable to render directory without index file")]
#[display(fmt = "unable to render directory without index file")]
IsDirectory,
}
@ -25,19 +25,19 @@ impl ResponseError for FilesError {
#[non_exhaustive]
pub enum UriSegmentError {
/// Segment started with the wrapped invalid character.
#[display("segment started with invalid character: ('{_0}')")]
#[display(fmt = "segment started with invalid character: ('{_0}')")]
BadStart(char),
/// Segment contained the wrapped invalid character.
#[display("segment contained invalid character ('{_0}')")]
#[display(fmt = "segment contained invalid character ('{_0}')")]
BadChar(char),
/// Segment ended with the wrapped invalid character.
#[display("segment ended with invalid character: ('{_0}')")]
#[display(fmt = "segment ended with invalid character: ('{_0}')")]
BadEnd(char),
/// Path is not a valid UTF-8 string after percent-decoding.
#[display("path is not a valid UTF-8 string after percent-decoding")]
#[display(fmt = "path is not a valid UTF-8 string after percent-decoding")]
NotValidUtf8,
}

View File

@ -235,7 +235,7 @@ impl Files {
/// request starts being handled by the file service, it will not be able to back-out and try
/// the next service, you will simply get a 404 (or 405) error response.
///
/// To allow `POST` requests to retrieve files, see [`Files::method_guard()`].
/// To allow `POST` requests to retrieve files, see [`Files::use_guards`].
///
/// # Examples
/// ```

View File

@ -11,7 +11,9 @@
//! .service(Files::new("/static", ".").prefer_utf8(true));
//! ```
#![warn(missing_docs, missing_debug_implementations)]
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
#![allow(clippy::uninlined_format_args)]
#![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))]
@ -74,7 +76,7 @@ mod tests {
dev::ServiceFactory,
guard,
http::{
header::{self, ContentDisposition, DispositionParam},
header::{self, ContentDisposition, DispositionParam, DispositionType},
Method, StatusCode,
},
middleware::Compress,
@ -306,11 +308,11 @@ mod tests {
let resp = file.respond_to(&req);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/javascript",
"application/javascript; charset=utf-8"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"test.js\"",
"inline; filename=\"test.js\""
);
}
@ -567,30 +569,6 @@ mod tests {
assert_eq!(bytes, data);
}
#[cfg(not(target_os = "windows"))]
#[actix_rt::test]
async fn test_static_files_with_special_characters() {
// Create the file we want to test against ad-hoc. We can't check it in as otherwise
// Windows can't even checkout this repository.
let temp_dir = tempfile::tempdir().unwrap();
let file_with_newlines = temp_dir.path().join("test\n\x0B\x0C\rnewline.text");
fs::write(&file_with_newlines, "Look at my newlines").unwrap();
let srv = test::init_service(
App::new().service(Files::new("/", temp_dir.path()).index_file("Cargo.toml")),
)
.await;
let request = TestRequest::get()
.uri("/test%0A%0B%0C%0Dnewline.text")
.to_request();
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::OK);
let bytes = test::read_body(response).await;
let data = web::Bytes::from(fs::read(file_with_newlines).unwrap());
assert_eq!(bytes, data);
}
#[actix_rt::test]
async fn test_files_not_allowed() {
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
@ -863,9 +841,9 @@ mod tests {
#[actix_rt::test]
async fn test_percent_encoding_2() {
let temp_dir = tempfile::tempdir().unwrap();
let tmpdir = tempfile::tempdir().unwrap();
let filename = match cfg!(unix) {
true => "ض:?#[]{}<>()@!$&'`|*+,;= %20\n.test",
true => "ض:?#[]{}<>()@!$&'`|*+,;= %20.test",
false => "ض#[]{}()@!$&'`+,;= %20.test",
};
let filename_encoded = filename
@ -875,9 +853,9 @@ mod tests {
write!(&mut buf, "%{:02X}", c).unwrap();
buf
});
std::fs::File::create(temp_dir.path().join(filename)).unwrap();
std::fs::File::create(tmpdir.path().join(filename)).unwrap();
let srv = test::init_service(App::new().service(Files::new("/", temp_dir.path()))).await;
let srv = test::init_service(App::new().service(Files::new("", tmpdir.path()))).await;
let req = TestRequest::get()
.uri(&format!("/{}", filename_encoded))

View File

@ -24,6 +24,7 @@ use bitflags::bitflags;
use derive_more::{Deref, DerefMut};
use futures_core::future::LocalBoxFuture;
use mime::Mime;
use mime_guess::from_path;
use crate::{encoding::equiv_utf8_text, range::HttpRange};
@ -127,7 +128,7 @@ impl NamedFile {
}
};
let ct = mime_guess::from_path(&path).first_or_octet_stream();
let ct = from_path(&path).first_or_octet_stream();
let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
@ -139,13 +140,7 @@ impl NamedFile {
_ => DispositionType::Attachment,
};
// replace special characters in filenames which could occur on some filesystems
let filename_s = filename
.replace('\n', "%0A") // \n line break
.replace('\x0B', "%0B") // \v vertical tab
.replace('\x0C', "%0C") // \f form feed
.replace('\r', "%0D"); // \r carriage return
let mut parameters = vec![DispositionParam::Filename(filename_s)];
let mut parameters = vec![DispositionParam::Filename(String::from(filename.as_ref()))];
if !filename.is_ascii() {
parameters.push(DispositionParam::FilenameExt(ExtendedValue {

View File

@ -79,7 +79,7 @@ impl FilesService {
let (req, _) = req.into_parts();
(self.renderer)(&dir, &req).unwrap_or_else(|err| ServiceResponse::from_err(err, req))
(self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req))
}
}

View File

@ -2,10 +2,6 @@
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.72.
## 3.2.0
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
## 3.1.0

View File

@ -1,11 +1,11 @@
[package]
name = "actix-http-test"
version = "3.2.0"
version = "3.1.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web"
repository = "https://github.com/actix/actix-web.git"
categories = [
"network-programming",
"asynchronous",
@ -18,17 +18,9 @@ edition = "2021"
[package.metadata.docs.rs]
features = []
[package.metadata.cargo_check_external_types]
allowed_external_types = [
"actix_codec::*",
"actix_http::*",
"actix_server::*",
"awc::*",
"bytes::*",
"futures_core::*",
"http::*",
"tokio::*",
]
[lib]
name = "actix_http_test"
path = "src/lib.rs"
[features]
default = []
@ -59,6 +51,3 @@ tokio = { version = "1.24.2", features = ["sync"] }
[dev-dependencies]
actix-http = "3"
[lints]
workspace = true

View File

@ -1,20 +1,17 @@
# `actix-http-test`
# actix-http-test
<!-- prettier-ignore-start -->
> Various helpers for Actix applications to use during testing.
[![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)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.1.0)](https://docs.rs/actix-http-test/3.1.0)
![Version](https://img.shields.io/badge/rustc-1.68+-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)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.1.0/status.svg)](https://deps.rs/crate/actix-http-test/3.1.0)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end -->
## Documentation & Resources
<!-- cargo-rdme start -->
Various helpers for Actix applications to use during testing.
<!-- cargo-rdme end -->
- [API Documentation](https://docs.rs/actix-http-test)
- Minimum Supported Rust Version (MSRV): 1.68

View File

@ -1,5 +1,8 @@
//! Various helpers for Actix applications to use during testing.
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow(clippy::uninlined_format_args)]
#![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))]
@ -106,7 +109,7 @@ pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
builder.set_verify(SslVerifyMode::NONE);
let _ = builder
.set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|err| log::error!("Can not set ALPN protocol: {err}"));
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
Connector::new()
.conn_lifetime(Duration::from_secs(0))

View File

@ -2,79 +2,11 @@
## Unreleased
## 3.10.0
### Added
- Add `header::CLEAR_SITE_DATA` constant.
- Add `Extensions::get_or_insert[_with]()` methods.
- Implement `From<Bytes>` for `Payload`.
- Implement `From<Vec<u8>>` for `Payload`.
### Changed
- Update `brotli` dependency to `7`.
- Minimum supported Rust version (MSRV) is now 1.75.
## 3.9.0
### Added
- Implement `FromIterator<(HeaderName, HeaderValue)>` for `HeaderMap`.
## 3.8.0
### Added
- Add `error::InvalidStatusCode` re-export.
## 3.7.0
### Added
- Add `rustls-0_23` crate feature
- Add `{h1::H1Service, h2::H2Service, HttpService}::rustls_0_23()` and `HttpService::rustls_0_23_with_config()` service constructors.
### Changed
- Update `brotli` dependency to `6`.
- Minimum supported Rust version (MSRV) is now 1.72.
## 3.6.0
### Added
- Add `rustls-0_22` crate feature.
- Add `{h1::H1Service, h2::H2Service, HttpService}::rustls_0_22()` and `HttpService::rustls_0_22_with_config()` service constructors.
- Implement `From<&HeaderMap>` for `http::HeaderMap`.
## 3.5.1
### Fixed
- Prevent hang when returning zero-sized response bodies through compression layer.
## 3.5.0
### Added
- Implement `From<HeaderMap>` for `http::HeaderMap`.
### Changed
- Updated `zstd` dependency to `0.13`.
### Fixed
- Prevent compression of zero-sized response bodies.
## 3.4.0
### Added
- Add `rustls-0_20` crate feature.
- Add `{h1::H1Service, h2::H2Service, HttpService}::rustls_021()` and `HttpService::rustls_021_with_config()` service constructors.
- Add `body::to_bytes_limited()` function.
- Add `{H1Service, H2Service, HttpService}::rustls_021()` and `HttpService::rustls_021_with_config()` service constructors.
- Add `body::to_body_limit()` function.
- Add `body::BodyLimitExceeded` error type.
### Changed

View File

@ -1,14 +1,14 @@
[package]
name = "actix-http"
version = "3.10.0"
version = "3.3.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "HTTP types and services for the Actix ecosystem"
description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web"
repository = "https://github.com/actix/actix-web.git"
categories = [
"network-programming",
"asynchronous",
@ -20,98 +20,60 @@ edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [
"http2",
"ws",
"openssl",
"rustls-0_20",
"rustls-0_21",
"rustls-0_22",
"rustls-0_23",
"compress-brotli",
"compress-gzip",
"compress-zstd",
]
# features that docs.rs will build with
features = ["http2", "ws", "openssl", "rustls-0_20", "rustls-0_21", "compress-brotli", "compress-gzip", "compress-zstd"]
[package.metadata.cargo_check_external_types]
allowed_external_types = [
"actix_codec::*",
"actix_service::*",
"actix_tls::*",
"actix_utils::*",
"bytes::*",
"bytestring::*",
"encoding_rs::*",
"futures_core::*",
"h2::*",
"http::*",
"httparse::*",
"language_tags::*",
"mime::*",
"openssl::*",
"rustls::*",
"tokio_util::*",
"tokio::*",
]
[lib]
name = "actix_http"
path = "src/lib.rs"
[features]
default = []
# HTTP/2 protocol support
http2 = ["dep:h2"]
http2 = ["h2"]
# WebSocket protocol implementation
ws = [
"dep:local-channel",
"dep:base64",
"dep:rand",
"dep:sha1",
"local-channel",
"base64",
"rand",
"sha1",
]
# TLS via OpenSSL
openssl = ["__tls", "actix-tls/accept", "actix-tls/openssl"]
openssl = ["actix-tls/accept", "actix-tls/openssl"]
# TLS via Rustls v0.20
rustls = ["__tls", "rustls-0_20"]
rustls = ["rustls-0_20"]
# TLS via Rustls v0.20
rustls-0_20 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_20"]
rustls-0_20 = ["actix-tls/accept", "actix-tls/rustls-0_20"]
# TLS via Rustls v0.21
rustls-0_21 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_21"]
# TLS via Rustls v0.22
rustls-0_22 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_22"]
# TLS via Rustls v0.23
rustls-0_23 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_23"]
rustls-0_21 = ["actix-tls/accept", "actix-tls/rustls-0_21"]
# Compression codecs
compress-brotli = ["__compress", "dep:brotli"]
compress-gzip = ["__compress", "dep:flate2"]
compress-zstd = ["__compress", "dep:zstd"]
compress-brotli = ["__compress", "brotli"]
compress-gzip = ["__compress", "flate2"]
compress-zstd = ["__compress", "zstd"]
# Internal (PRIVATE!) features used to aid testing and checking feature status.
# Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime.
__compress = []
# Internal (PRIVATE!) features used to aid checking feature status.
# Don't rely on these whatsoever. They may disappear at anytime.
__tls = []
[dependencies]
actix-service = "2"
actix-codec = "0.5"
actix-utils = "3"
actix-rt = { version = "2.2", default-features = false }
ahash = "0.8"
bitflags = "2"
bytes = "1"
bytestring = "1"
derive_more = { version = "2", features = ["as_ref", "deref", "deref_mut", "display", "error", "from"] }
derive_more = "0.99.5"
encoding_rs = "0.8"
foldhash = "0.1"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http = "0.2.7"
httparse = "1.5.1"
@ -127,62 +89,54 @@ tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# http2
h2 = { version = "0.3.26", optional = true }
h2 = { version = "0.3.17", optional = true }
# websockets
local-channel = { version = "0.1", optional = true }
base64 = { version = "0.22", optional = true }
rand = { version = "0.9", optional = true }
base64 = { version = "0.21", optional = true }
rand = { version = "0.8", optional = true }
sha1 = { version = "0.10", optional = true }
# openssl/rustls
actix-tls = { version = "3.4", default-features = false, optional = true }
actix-tls = { version = "3.1", default-features = false, optional = true }
# compress-*
brotli = { version = "7", optional = true }
brotli = { version = "3.3.3", optional = true }
flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.13", optional = true }
zstd = { version = "0.12", optional = true }
[dev-dependencies]
actix-http-test = { version = "3", features = ["openssl"] }
actix-server = "2"
actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23-webpki-roots"] }
actix-tls = { version = "3.1", features = ["openssl"] }
actix-web = "4"
async-stream = "0.3"
criterion = { version = "0.5", features = ["html_reports"] }
divan = "0.1.8"
env_logger = "0.11"
env_logger = "0.10"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
memchr = "2.4"
once_cell = "1.21"
rcgen = "0.13"
once_cell = "1.9"
rcgen = "0.11"
regex = "1.3"
rustversion = "1"
rustls-pemfile = "2"
serde = { version = "1", features = ["derive"] }
rustls-pemfile = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.55" }
tls-rustls_023 = { package = "rustls", version = "0.23" }
tls-rustls_021 = { package = "rustls", version = "0.21" }
tokio = { version = "1.24.2", features = ["net", "rt", "macros"] }
[lints]
workspace = true
[[example]]
name = "ws"
required-features = ["ws", "rustls-0_23"]
required-features = ["ws", "rustls-0_21"]
[[example]]
name = "tls_rustls"
required-features = ["http2", "rustls-0_23"]
required-features = ["http2", "rustls-0_21"]
[[bench]]
name = "response-body-compression"
harness = false
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
[[bench]]
name = "date-formatting"
harness = false

View File

@ -1,21 +1,22 @@
# `actix-http`
# actix-http
> HTTP types and services for the Actix ecosystem.
<!-- prettier-ignore-start -->
> HTTP primitives for the Actix ecosystem.
[![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.10.0)](https://docs.rs/actix-http/3.10.0)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.3.1)](https://docs.rs/actix-http/3.3.1)
![Version](https://img.shields.io/badge/rustc-1.68+-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.10.0/status.svg)](https://deps.rs/crate/actix-http/3.10.0)
[![dependency status](https://deps.rs/crate/actix-http/3.3.1/status.svg)](https://deps.rs/crate/actix-http/3.3.1)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end -->
## Documentation & Resources
## Examples
- [API Documentation](https://docs.rs/actix-http)
- Minimum Supported Rust Version (MSRV): 1.68
## Example
```rust
use std::{env, io};

View File

@ -1,20 +0,0 @@
use std::time::SystemTime;
use actix_http::header::HttpDate;
use divan::{black_box, AllocProfiler, Bencher};
#[global_allocator]
static ALLOC: AllocProfiler = AllocProfiler::system();
#[divan::bench]
fn date_formatting(b: Bencher<'_, '_>) {
let now = SystemTime::now();
b.bench(|| {
black_box(HttpDate::from(black_box(now)).to_string());
})
}
fn main() {
divan::main();
}

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::convert::Infallible;
use actix_http::{encoding::Encoder, ContentEncoding, Request, Response, StatusCode};

View File

@ -1,10 +1,10 @@
use actix_http::HttpService;
use actix_server::Server;
use actix_service::map_config;
use actix_web::{dev::AppConfig, get, App, Responder};
use actix_web::{dev::AppConfig, get, App};
#[get("/")]
async fn index() -> impl Responder {
async fn index() -> &'static str {
"Hello, world. From Actix Web!"
}

View File

@ -23,7 +23,7 @@ async fn main() -> io::Result<()> {
body.extend_from_slice(&item?);
}
info!("request body: {body:?}");
info!("request body: {:?}", body);
let res = Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
@ -31,7 +31,8 @@ async fn main() -> io::Result<()> {
Ok::<_, Error>(res)
})
.tcp() // No TLS
// No TLS
.tcp()
})?
.run()
.await

View File

@ -8,7 +8,7 @@
use std::{convert::Infallible, io};
use actix_http::{body::BodyStream, HttpService, Request, Response, StatusCode};
use actix_http::{HttpService, Request, Response, StatusCode};
use actix_server::Server;
#[tokio::main(flavor = "current_thread")]
@ -19,12 +19,7 @@ async fn main() -> io::Result<()> {
.bind("h2c-detect", ("127.0.0.1", 8080), || {
HttpService::build()
.finish(|_req: Request| async move {
Ok::<_, Infallible>(Response::build(StatusCode::OK).body(BodyStream::new(
futures_util::stream::iter([
Ok::<_, String>("123".into()),
Err("wertyuikmnbvcxdfty6t".to_owned()),
]),
)))
Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!"))
})
.tcp_auto_h2c()
})?

View File

@ -17,7 +17,7 @@ async fn main() -> io::Result<()> {
ext.insert(42u32);
})
.finish(|req: Request| async move {
info!("{req:?}");
info!("{:?}", req);
let mut res = Response::build(StatusCode::OK);
res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));

View File

@ -22,16 +22,16 @@ async fn main() -> io::Result<()> {
.bind("streaming-error", ("127.0.0.1", 8080), || {
HttpService::build()
.finish(|req| async move {
info!("{req:?}");
info!("{:?}", req);
let res = Response::ok();
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
yield Ok(Bytes::from("123"));
yield Ok(Bytes::from("456"));
actix_rt::time::sleep(Duration::from_secs(1)).await;
actix_rt::time::sleep(Duration::from_millis(1000)).await;
yield Err(io::Error::new(io::ErrorKind::Other, "abc"));
yield Err(io::Error::new(io::ErrorKind::Other, ""));
})))
})
.tcp()

View File

@ -12,7 +12,7 @@
//! Protocol: HTTP/1.1
//! ```
extern crate tls_rustls_023 as rustls;
extern crate tls_rustls_021 as rustls;
use std::io;
@ -36,34 +36,31 @@ async fn main() -> io::Result<()> {
);
ok::<_, Error>(Response::ok().set_body(body))
})
.rustls_0_23(rustls_config())
.rustls_021(rustls_config())
})?
.run()
.await
}
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 = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
let cert_file = cert.serialize_pem().unwrap();
let key_file = cert.serialize_private_key_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();
.unwrap()
.into_iter()
.map(rustls::Certificate)
.collect();
let mut keys = rustls_pemfile::pkcs8_private_keys(key_file).unwrap();
let mut config = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(
cert_chain,
rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
)
.with_single_cert(cert_chain, rustls::PrivateKey(keys.remove(0)))
.unwrap();
const H1_ALPN: &[u8] = b"http/1.1";

View File

@ -1,7 +1,7 @@
//! Sets up a WebSocket server over TCP and TLS.
//! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames.
extern crate tls_rustls_023 as rustls;
extern crate tls_rustls_021 as rustls;
use std::{
io,
@ -17,6 +17,7 @@ use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use futures_core::{ready, Stream};
use tokio_util::codec::Encoder;
use tracing::{info, trace};
#[actix_rt::main]
async fn main() -> io::Result<()> {
@ -29,19 +30,19 @@ async fn main() -> io::Result<()> {
.bind("tls", ("127.0.0.1", 8443), || {
HttpService::build()
.finish(handler)
.rustls_0_23(tls_config())
.rustls_021(tls_config())
})?
.run()
.await
}
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
tracing::info!("handshaking");
info!("handshaking");
let mut res = ws::handshake(req.head())?;
// handshake will always fail under HTTP/2
tracing::info!("responding");
info!("responding");
res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))
}
@ -63,7 +64,7 @@ impl Stream for Heartbeat {
type Item = Result<Bytes, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
tracing::trace!("poll");
trace!("poll");
ready!(self.as_mut().interval.poll_tick(cx));
@ -84,27 +85,27 @@ impl Stream for Heartbeat {
fn tls_config() -> rustls::ServerConfig {
use std::io::BufReader;
use rustls::{Certificate, PrivateKey};
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 = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
let cert_file = cert.serialize_pem().unwrap();
let key_file = cert.serialize_private_key_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 = certs(cert_file)
.unwrap()
.into_iter()
.map(Certificate)
.collect();
let mut keys = pkcs8_private_keys(key_file).unwrap();
let mut config = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(
cert_chain,
rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
)
.with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
.unwrap();
config.alpn_protocols.push(b"http/1.1".to_vec());

View File

@ -131,7 +131,7 @@ mod tests {
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
}
#[derive(Debug, Display, Error)]
#[display("stream error")]
#[display(fmt = "stream error")]
struct StreamErr;
#[actix_rt::test]

View File

@ -531,6 +531,7 @@ where
mod tests {
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use futures_util::stream;
use super::*;

View File

@ -38,7 +38,7 @@ pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
/// Error type returned from [`to_bytes_limited`] when body produced exceeds limit.
#[derive(Debug, Display, Error)]
#[display("limit exceeded while collecting body bytes")]
#[display(fmt = "limit exceeded while collecting body bytes")]
#[non_exhaustive]
pub struct BodyLimitExceeded;

View File

@ -28,7 +28,7 @@ impl Date {
fn update(&mut self) {
self.pos = 0;
write!(self, "{}", httpdate::HttpDate::from(SystemTime::now())).unwrap();
write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap();
}
}

View File

@ -191,7 +191,7 @@ impl ContentDecoder {
Ok(None)
}
}
Err(err) => Err(err),
Err(e) => Err(e),
},
#[cfg(feature = "compress-gzip")]
@ -205,7 +205,7 @@ impl ContentDecoder {
Ok(None)
}
}
Err(err) => Err(err),
Err(e) => Err(e),
},
#[cfg(feature = "compress-gzip")]
@ -218,7 +218,7 @@ impl ContentDecoder {
Ok(None)
}
}
Err(err) => Err(err),
Err(e) => Err(e),
},
#[cfg(feature = "compress-zstd")]
@ -231,7 +231,7 @@ impl ContentDecoder {
Ok(None)
}
}
Err(err) => Err(err),
Err(e) => Err(e),
},
}
}
@ -250,7 +250,7 @@ impl ContentDecoder {
Ok(None)
}
}
Err(err) => Err(err),
Err(e) => Err(e),
},
#[cfg(feature = "compress-gzip")]
@ -265,7 +265,7 @@ impl ContentDecoder {
Ok(None)
}
}
Err(err) => Err(err),
Err(e) => Err(e),
},
#[cfg(feature = "compress-gzip")]
@ -280,7 +280,7 @@ impl ContentDecoder {
Ok(None)
}
}
Err(err) => Err(err),
Err(e) => Err(e),
},
#[cfg(feature = "compress-zstd")]
@ -295,7 +295,7 @@ impl ContentDecoder {
Ok(None)
}
}
Err(err) => Err(err),
Err(e) => Err(e),
},
}
}

View File

@ -50,21 +50,10 @@ impl<B: MessageBody> Encoder<B> {
}
}
fn empty() -> Self {
Encoder {
body: EncoderBody::Full { body: Bytes::new() },
encoder: None,
fut: None,
eof: true,
}
}
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
// no need to compress empty bodies
match body.size() {
BodySize::None => return Self::none(),
BodySize::Sized(0) => return Self::empty(),
_ => {}
// no need to compress an empty body
if matches!(body.size(), BodySize::None) {
return Self::none();
}
let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
@ -415,11 +404,11 @@ fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
#[non_exhaustive]
pub enum EncoderError {
/// Wrapped body stream error.
#[display("body")]
#[display(fmt = "body")]
Body(Box<dyn StdError>),
/// Generic I/O error.
#[display("io")]
#[display(fmt = "io")]
Io(io::Error),
}

View File

@ -3,7 +3,7 @@
use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error};
use derive_more::{Display, Error, From};
pub use http::{status::InvalidStatusCode, Error as HttpError};
pub use http::Error as HttpError;
use http::{uri::InvalidUri, StatusCode};
use crate::{body::BoxBody, Response};
@ -80,28 +80,28 @@ impl From<Error> for Response<BoxBody> {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
pub(crate) enum Kind {
#[display("error processing HTTP")]
#[display(fmt = "error processing HTTP")]
Http,
#[display("error parsing HTTP message")]
#[display(fmt = "error parsing HTTP message")]
Parse,
#[display("request payload read error")]
#[display(fmt = "request payload read error")]
Payload,
#[display("response body write error")]
#[display(fmt = "response body write error")]
Body,
#[display("send response error")]
#[display(fmt = "send response error")]
SendResponse,
#[display("error in WebSocket process")]
#[display(fmt = "error in WebSocket process")]
Ws,
#[display("connection error")]
#[display(fmt = "connection error")]
Io,
#[display("encoder error")]
#[display(fmt = "encoder error")]
Encoder,
}
@ -160,44 +160,44 @@ impl From<crate::ws::ProtocolError> for Error {
#[non_exhaustive]
pub enum ParseError {
/// An invalid `Method`, such as `GE.T`.
#[display("invalid method specified")]
#[display(fmt = "invalid method specified")]
Method,
/// An invalid `Uri`, such as `exam ple.domain`.
#[display("URI error: {}", _0)]
#[display(fmt = "URI error: {}", _0)]
Uri(InvalidUri),
/// An invalid `HttpVersion`, such as `HTP/1.1`
#[display("invalid HTTP version specified")]
#[display(fmt = "invalid HTTP version specified")]
Version,
/// An invalid `Header`.
#[display("invalid Header provided")]
#[display(fmt = "invalid Header provided")]
Header,
/// A message head is too large to be reasonable.
#[display("message head is too large")]
#[display(fmt = "message head is too large")]
TooLarge,
/// A message reached EOF, but is not complete.
#[display("message is incomplete")]
#[display(fmt = "message is incomplete")]
Incomplete,
/// An invalid `Status`, such as `1337 ELITE`.
#[display("invalid status provided")]
#[display(fmt = "invalid status provided")]
Status,
/// A timeout occurred waiting for an IO event.
#[allow(dead_code)]
#[display("timeout")]
#[display(fmt = "timeout")]
Timeout,
/// An I/O error that occurred while trying to read or write to a network stream.
#[display("I/O error: {}", _0)]
#[display(fmt = "I/O error: {}", _0)]
Io(io::Error),
/// Parsing a field as string failed.
#[display("UTF-8 error: {}", _0)]
#[display(fmt = "UTF-8 error: {}", _0)]
Utf8(Utf8Error),
}
@ -256,28 +256,28 @@ impl From<ParseError> for Response<BoxBody> {
#[non_exhaustive]
pub enum PayloadError {
/// A payload reached EOF, but is not complete.
#[display("payload reached EOF before completing: {:?}", _0)]
#[display(fmt = "payload reached EOF before completing: {:?}", _0)]
Incomplete(Option<io::Error>),
/// Content encoding stream corruption.
#[display("can not decode content-encoding")]
#[display(fmt = "can not decode content-encoding")]
EncodingCorrupted,
/// Payload reached size limit.
#[display("payload reached size limit")]
#[display(fmt = "payload reached size limit")]
Overflow,
/// Payload length is unknown.
#[display("payload length is unknown")]
#[display(fmt = "payload length is unknown")]
UnknownLength,
/// HTTP/2 payload error.
#[cfg(feature = "http2")]
#[display("{}", _0)]
#[display(fmt = "{}", _0)]
Http2Payload(::h2::Error),
/// Generic I/O error.
#[display("{}", _0)]
#[display(fmt = "{}", _0)]
Io(io::Error),
}
@ -326,44 +326,44 @@ impl From<PayloadError> for Error {
#[non_exhaustive]
pub enum DispatchError {
/// Service error.
#[display("service error")]
#[display(fmt = "service error")]
Service(Response<BoxBody>),
/// Body streaming error.
#[display("body error: {}", _0)]
#[display(fmt = "body error: {}", _0)]
Body(Box<dyn StdError>),
/// Upgrade service error.
#[display("upgrade error")]
#[display(fmt = "upgrade error")]
Upgrade,
/// An `io::Error` that occurred while trying to read or write to a network stream.
#[display("I/O error: {}", _0)]
#[display(fmt = "I/O error: {}", _0)]
Io(io::Error),
/// Request parse error.
#[display("request parse error: {}", _0)]
#[display(fmt = "request parse error: {}", _0)]
Parse(ParseError),
/// HTTP/2 error.
#[display("{}", _0)]
#[display(fmt = "{}", _0)]
#[cfg(feature = "http2")]
H2(h2::Error),
/// The first request did not complete within the specified timeout.
#[display("request did not complete within the specified timeout")]
#[display(fmt = "request did not complete within the specified timeout")]
SlowRequestTimeout,
/// Disconnect timeout. Makes sense for TLS streams.
#[display("connection shutdown timeout")]
#[display(fmt = "connection shutdown timeout")]
DisconnectTimeout,
/// Handler dropped payload before reading EOF.
#[display("handler dropped payload before reading EOF")]
#[display(fmt = "handler dropped payload before reading EOF")]
HandlerDroppedPayload,
/// Internal error.
#[display("internal error")]
#[display(fmt = "internal error")]
InternalError,
}
@ -389,17 +389,19 @@ impl StdError for DispatchError {
#[non_exhaustive]
pub enum ContentTypeError {
/// Can not parse content type.
#[display("could not parse content type")]
#[display(fmt = "could not parse content type")]
ParseError,
/// Unknown content encoding.
#[display("unknown content encoding")]
#[display(fmt = "unknown content encoding")]
UnknownEncoding,
}
#[cfg(test)]
mod tests {
use http::Error as HttpError;
use std::io;
use http::{Error as HttpError, StatusCode};
use super::*;

View File

@ -31,7 +31,7 @@ impl Hasher for NoOpHasher {
/// All entries into this map must be owned types (or static references).
#[derive(Default)]
pub struct Extensions {
// use no-op hasher with a std HashMap with for faster lookups on the small `TypeId` keys
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
map: HashMap<TypeId, Box<dyn Any>, BuildHasherDefault<NoOpHasher>>,
}
@ -104,46 +104,6 @@ impl Extensions {
.and_then(|boxed| boxed.downcast_mut())
}
/// Inserts the given `value` into the extensions if it is not present, then returns a reference
/// to the value in the extensions.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// assert_eq!(map.get::<Vec<u32>>(), None);
///
/// map.get_or_insert(Vec::<u32>::new()).push(1);
/// assert_eq!(map.get::<Vec<u32>>(), Some(&vec![1]));
///
/// map.get_or_insert(Vec::<u32>::new()).push(2);
/// assert_eq!(map.get::<Vec<u32>>(), Some(&vec![1,2]));
/// ```
pub fn get_or_insert<T: 'static>(&mut self, value: T) -> &mut T {
self.get_or_insert_with(|| value)
}
/// Inserts a value computed from `f` into the extensions if the given `value` is not present,
/// then returns a reference to the value in the extensions.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// assert_eq!(map.get::<Vec<u32>>(), None);
///
/// map.get_or_insert_with(Vec::<u32>::new).push(1);
/// assert_eq!(map.get::<Vec<u32>>(), Some(&vec![1]));
///
/// map.get_or_insert_with(Vec::<u32>::new).push(2);
/// assert_eq!(map.get::<Vec<u32>>(), Some(&vec![1,2]));
/// ```
pub fn get_or_insert_with<T: 'static, F: FnOnce() -> T>(&mut self, default: F) -> &mut T {
self.map
.entry(TypeId::of::<T>())
.or_insert_with(|| Box::new(default()))
.downcast_mut()
.expect("extensions map should now contain a T value")
}
/// Remove an item from the map of a given type.
///
/// If an item of this type was already stored, it will be returned.

View File

@ -198,6 +198,9 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
#[cfg(test)]
mod tests {
use bytes::BytesMut;
use http::Method;
use super::*;
use crate::HttpMessage as _;

View File

@ -532,7 +532,7 @@ impl Decoder for PayloadDecoder {
*state = match state.step(src, size, &mut buf) {
Poll::Pending => return Ok(None),
Poll::Ready(Ok(state)) => state,
Poll::Ready(Err(err)) => return Err(err),
Poll::Ready(Err(e)) => return Err(e),
};
if *state == ChunkedState::End {
@ -563,8 +563,15 @@ impl Decoder for PayloadDecoder {
#[cfg(test)]
mod tests {
use bytes::{Bytes, BytesMut};
use http::{Method, Version};
use super::*;
use crate::{header::SET_COOKIE, HttpMessage as _};
use crate::{
error::ParseError,
header::{HeaderName, SET_COOKIE},
HttpMessage as _,
};
impl PayloadType {
pub(crate) fn unwrap(self) -> PayloadDecoder {

View File

@ -512,10 +512,8 @@ where
}
Poll::Ready(Some(Err(err))) => {
let err = err.into();
tracing::error!("Response payload stream error: {err:?}");
this.flags.insert(Flags::FINISHED);
return Err(DispatchError::Body(err));
return Err(DispatchError::Body(err.into()));
}
Poll::Pending => return Ok(PollResponse::DoNothing),
@ -551,7 +549,6 @@ where
}
Poll::Ready(Some(Err(err))) => {
tracing::error!("Response payload stream error: {err:?}");
this.flags.insert(Flags::FINISHED);
return Err(DispatchError::Body(
Error::new_body().with_cause(err).into(),
@ -706,7 +703,7 @@ where
req.head_mut().peer_addr = *this.peer_addr;
req.conn_data.clone_from(this.conn_data);
req.conn_data = this.conn_data.as_ref().map(Rc::clone);
match this.codec.message_type() {
// request has no payload

View File

@ -313,7 +313,7 @@ impl MessageType for RequestHeadType {
_ => return Err(io::Error::new(io::ErrorKind::Other, "unsupported version")),
}
)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}
}
@ -433,7 +433,7 @@ impl TransferEncoding {
buf.extend_from_slice(b"0\r\n\r\n");
} else {
writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len())
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
buf.reserve(msg.len() + 2);
buf.extend_from_slice(msg);

View File

@ -153,7 +153,7 @@ mod openssl {
}
#[cfg(feature = "rustls-0_20")]
mod rustls_0_20 {
mod rustls_020 {
use std::io;
use actix_service::ServiceFactoryExt as _;
@ -214,7 +214,7 @@ mod rustls_0_20 {
}
#[cfg(feature = "rustls-0_21")]
mod rustls_0_21 {
mod rustls_021 {
use std::io;
use actix_service::ServiceFactoryExt as _;
@ -274,128 +274,6 @@ mod rustls_0_21 {
}
}
#[cfg(feature = "rustls-0_22")]
mod rustls_0_22 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.22 based service.
pub fn rustls_0_22(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
#[cfg(feature = "rustls-0_23")]
mod rustls_0_23 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.23 based service.
pub fn rustls_0_23(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
@ -480,15 +358,15 @@ where
let cfg = self.cfg.clone();
Box::pin(async move {
let expect = expect.await.map_err(|err| {
tracing::error!("Initialization of HTTP expect service error: {err:?}");
})?;
let expect = expect
.await
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
let upgrade = match upgrade {
Some(upgrade) => {
let upgrade = upgrade.await.map_err(|err| {
tracing::error!("Initialization of HTTP upgrade service error: {err:?}");
})?;
let upgrade = upgrade
.await
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
Some(upgrade)
}
None => None,
@ -496,7 +374,7 @@ where
let service = service
.await
.map_err(|err| error!("Initialization of HTTP service error: {err:?}"))?;
.map_err(|e| error!("Init http service error: {:?}", e))?;
Ok(H1ServiceHandler::new(
cfg,
@ -541,6 +419,6 @@ where
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
Dispatcher::new(io, Rc::clone(&self.flow), self.cfg.clone(), addr, conn_data)
Dispatcher::new(io, self.flow.clone(), self.cfg.clone(), addr, conn_data)
}
}

View File

@ -4,7 +4,7 @@ use std::{
future::Future,
marker::PhantomData,
net,
pin::{pin, Pin},
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
@ -20,6 +20,7 @@ use h2::{
Ping, PingPong,
};
use pin_project_lite::pin_project;
use tracing::{error, trace, warn};
use crate::{
body::{BodySize, BoxBody, MessageBody},
@ -126,7 +127,7 @@ where
head.headers = parts.headers.into();
head.peer_addr = this.peer_addr;
req.conn_data.clone_from(&this.conn_data);
req.conn_data = this.conn_data.as_ref().map(Rc::clone);
let fut = this.flow.service.call(req);
let config = this.config.clone();
@ -146,13 +147,11 @@ where
if let Err(err) = res {
match err {
DispatchError::SendResponse(err) => {
tracing::trace!("Error sending response: {err:?}");
}
DispatchError::SendData(err) => {
tracing::warn!("Send data error: {err:?}");
trace!("Error sending HTTP/2 response: {:?}", err)
}
DispatchError::SendData(err) => warn!("{:?}", err),
DispatchError::ResponseBody(err) => {
tracing::error!("Response payload stream error: {err:?}");
error!("Response payload stream error: {:?}", err)
}
}
}
@ -229,9 +228,9 @@ where
return Ok(());
}
let mut body = pin!(body);
// poll response body and send chunks to client
actix_rt::pin!(body);
while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?;

View File

@ -141,7 +141,7 @@ mod openssl {
}
#[cfg(feature = "rustls-0_20")]
mod rustls_0_20 {
mod rustls_020 {
use std::io;
use actix_service::ServiceFactoryExt as _;
@ -192,7 +192,7 @@ mod rustls_0_20 {
}
#[cfg(feature = "rustls-0_21")]
mod rustls_0_21 {
mod rustls_021 {
use std::io;
use actix_service::ServiceFactoryExt as _;
@ -242,108 +242,6 @@ mod rustls_0_21 {
}
}
#[cfg(feature = "rustls-0_22")]
mod rustls_0_22 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
/// Create Rustls v0.22 based service.
pub fn rustls_0_22(
self,
mut config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError,
> {
let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
#[cfg(feature = "rustls-0_23")]
mod rustls_0_23 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
/// Create Rustls v0.23 based service.
pub fn rustls_0_23(
self,
mut config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError,
> {
let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
@ -434,7 +332,7 @@ where
H2ServiceHandlerResponse {
state: State::Handshake(
Some(Rc::clone(&self.flow)),
Some(self.flow.clone()),
Some(self.cfg.clone()),
addr,
on_connect_data,

View File

@ -18,14 +18,6 @@ pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status");
// TODO(breaking): replace with http's version
pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control");
/// Response header field that sends a signal to the user agent that it ought to remove all data of
/// a certain set of types.
///
/// See the [W3C Clear-Site-Data spec] for full semantics.
///
/// [W3C Clear-Site-Data spec]: https://www.w3.org/TR/clear-site-data/#header
pub const CLEAR_SITE_DATA: HeaderName = HeaderName::from_static("clear-site-data");
/// Response header that prevents a document from loading any cross-origin resources that don't
/// explicitly grant the document permission (using [CORP] or [CORS]).
///

View File

@ -2,7 +2,7 @@
use std::{borrow::Cow, collections::hash_map, iter, ops};
use foldhash::{HashMap as FoldHashMap, HashMapExt as _};
use ahash::AHashMap;
use http::header::{HeaderName, HeaderValue};
use smallvec::{smallvec, SmallVec};
@ -13,9 +13,8 @@ use super::AsHeaderName;
/// `HeaderMap` is a "multi-map" of [`HeaderName`] to one or more [`HeaderValue`]s.
///
/// # Examples
///
/// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// use actix_http::header::{self, HeaderMap, HeaderValue};
///
/// let mut map = HeaderMap::new();
///
@ -30,24 +29,9 @@ use super::AsHeaderName;
///
/// assert!(!map.contains_key(header::ORIGIN));
/// ```
///
/// Construct a header map using the [`FromIterator`] implementation. Note that it uses the append
/// strategy, so duplicate header names are preserved.
///
/// ```
/// use actix_http::header::{self, HeaderMap, HeaderValue};
///
/// let headers = HeaderMap::from_iter([
/// (header::CONTENT_TYPE, HeaderValue::from_static("text/plain")),
/// (header::COOKIE, HeaderValue::from_static("foo=1")),
/// (header::COOKIE, HeaderValue::from_static("bar=1")),
/// ]);
///
/// assert_eq!(headers.len(), 3);
/// ```
#[derive(Debug, Clone, Default)]
pub struct HeaderMap {
pub(crate) inner: FoldHashMap<HeaderName, Value>,
pub(crate) inner: AHashMap<HeaderName, Value>,
}
/// A bespoke non-empty list for HeaderMap values.
@ -116,7 +100,7 @@ impl HeaderMap {
/// ```
pub fn with_capacity(capacity: usize) -> Self {
HeaderMap {
inner: FoldHashMap::with_capacity(capacity),
inner: AHashMap::with_capacity(capacity),
}
}
@ -384,8 +368,8 @@ impl HeaderMap {
/// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
/// assert!(!removed.is_empty());
/// ```
pub fn insert(&mut self, name: HeaderName, val: HeaderValue) -> Removed {
let value = self.inner.insert(name, Value::one(val));
pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed {
let value = self.inner.insert(key, Value::one(val));
Removed::new(value)
}
@ -652,34 +636,10 @@ impl<'a> IntoIterator for &'a HeaderMap {
}
}
impl FromIterator<(HeaderName, HeaderValue)> for HeaderMap {
fn from_iter<T: IntoIterator<Item = (HeaderName, HeaderValue)>>(iter: T) -> Self {
iter.into_iter()
.fold(Self::new(), |mut map, (name, value)| {
map.append(name, value);
map
})
}
}
/// Convert a `http::HeaderMap` to our `HeaderMap`.
/// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> Self {
Self::from_drain(map.drain())
}
}
/// Convert our `HeaderMap` to a `http::HeaderMap`.
impl From<HeaderMap> for http::HeaderMap {
fn from(map: HeaderMap) -> Self {
Self::from_iter(map)
}
}
/// Convert our `&HeaderMap` to a `http::HeaderMap`.
impl From<&HeaderMap> for http::HeaderMap {
fn from(map: &HeaderMap) -> Self {
map.to_owned().into()
fn from(mut map: http::HeaderMap) -> HeaderMap {
HeaderMap::from_drain(map.drain())
}
}
@ -830,7 +790,7 @@ impl<'a> Drain<'a> {
}
}
impl Iterator for Drain<'_> {
impl<'a> Iterator for Drain<'a> {
type Item = (Option<HeaderName>, HeaderValue);
fn next(&mut self) -> Option<Self::Item> {

View File

@ -42,9 +42,9 @@ pub use self::{
as_name::AsHeaderName,
// re-export list is explicit so that any updates to `http` do not conflict with this set
common::{
CACHE_STATUS, CDN_CACHE_CONTROL, CLEAR_SITE_DATA, CROSS_ORIGIN_EMBEDDER_POLICY,
CROSS_ORIGIN_OPENER_POLICY, CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY,
X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO,
CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY,
CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST,
X_FORWARDED_PROTO,
},
into_pair::TryIntoHeaderPair,
into_value::TryIntoHeaderValue,

View File

@ -11,7 +11,7 @@ use crate::{
/// Error returned when a content encoding is unknown.
#[derive(Debug, Display, Error)]
#[display("unsupported content encoding")]
#[display(fmt = "unsupported content encoding")]
pub struct ContentEncodingParseError;
/// Represents a supported content encoding.

View File

@ -24,7 +24,8 @@ impl FromStr for HttpDate {
impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
httpdate::HttpDate::from(self.0).fmt(f)
let date_str = httpdate::fmt_http_date(self.0);
f.write_str(&date_str)
}
}
@ -36,7 +37,7 @@ impl TryIntoHeaderValue for HttpDate {
let mut wrt = MutWriter(&mut buf);
// unwrap: date output is known to be well formed and of known length
write!(wrt, "{}", self).unwrap();
write!(wrt, "{}", httpdate::fmt_http_date(self.0)).unwrap();
HeaderValue::from_maybe_shared(buf.split().freeze())
}

View File

@ -125,7 +125,7 @@ pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Re
}
#[derive(Debug, Clone, Display, Error)]
#[display("quality out of bounds")]
#[display(fmt = "quality out of bounds")]
#[non_exhaustive]
pub struct QualityOutOfBounds;

View File

@ -80,18 +80,18 @@ mod tests {
#[test]
fn comma_delimited_parsing() {
let headers = [];
let headers = vec![];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![0; 0]);
let headers = [
let headers = vec![
HeaderValue::from_static("1, 2"),
HeaderValue::from_static("3,4"),
];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![1, 2, 3, 4]);
let headers = [
let headers = vec![
HeaderValue::from_static(""),
HeaderValue::from_static(","),
HeaderValue::from_static(" "),

View File

@ -61,7 +61,7 @@ pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B, camel_case: bool) {
/// perform a remaining length check before writing.
pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B);
impl<B> io::Write for MutWriter<'_, B>
impl<'a, B> io::Write for MutWriter<'a, B>
where
B: BufMut,
{

View File

@ -103,7 +103,7 @@ pub trait HttpMessage: Sized {
}
}
impl<T> HttpMessage for &mut T
impl<'a, T> HttpMessage for &'a mut T
where
T: HttpMessage,
{

View File

@ -1,15 +1,11 @@
//! HTTP types and services for the Actix ecosystem.
//! HTTP primitives for the Actix ecosystem.
//!
//! ## Crate Features
//!
//! | Feature | Functionality |
//! | ------------------- | ------------------------------------------- |
//! | `http2` | HTTP/2 support via [h2]. |
//! | `openssl` | TLS support via [OpenSSL]. |
//! | `rustls-0_20` | TLS support via rustls 0.20. |
//! | `rustls-0_21` | TLS support via rustls 0.21. |
//! | `rustls-0_22` | TLS support via rustls 0.22. |
//! | `rustls-0_23` | TLS support via [rustls] 0.23. |
//! | `rustls` | TLS support via [rustls]. |
//! | `compress-brotli` | Payload compression support: Brotli. |
//! | `compress-gzip` | Payload compression support: Deflate, Gzip. |
//! | `compress-zstd` | Payload compression support: Zstd. |
@ -20,16 +16,19 @@
//! [rustls]: https://crates.io/crates/rustls
//! [trust-dns]: https://crates.io/crates/trust-dns
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow(
clippy::type_complexity,
clippy::too_many_arguments,
clippy::borrow_interior_mutable_const
clippy::borrow_interior_mutable_const,
clippy::uninlined_format_args
)]
#![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))]
pub use http::{uri, uri::Uri, Method, StatusCode, Version};
pub use ::http::{uri, uri::Uri, Method, StatusCode, Version};
pub mod body;
mod builder;
@ -59,7 +58,7 @@ pub mod ws;
#[allow(deprecated)]
pub use self::payload::PayloadStream;
#[cfg(feature = "__tls")]
#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))]
pub use self::service::TlsAcceptorConfig;
pub use self::{
builder::HttpServiceBuilder,

View File

@ -66,7 +66,7 @@ impl<T: Head> ops::DerefMut for Message<T> {
impl<T: Head> Drop for Message<T> {
fn drop(&mut self) {
T::with_pool(|p| p.release(Rc::clone(&self.head)))
T::with_pool(|p| p.release(self.head.clone()))
}
}

View File

@ -5,7 +5,7 @@
use std::cell::RefCell;
thread_local! {
static NOTIFY_DROPPED: RefCell<Option<bool>> = const { RefCell::new(None) };
static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
}
/// Check if the spawned task is dropped.

View File

@ -41,31 +41,13 @@ pin_project! {
}
impl<S> From<crate::h1::Payload> for Payload<S> {
#[inline]
fn from(payload: crate::h1::Payload) -> Self {
Payload::H1 { payload }
}
}
impl<S> From<Bytes> for Payload<S> {
#[inline]
fn from(bytes: Bytes) -> Self {
let (_, mut pl) = crate::h1::Payload::create(true);
pl.unread_data(bytes);
self::Payload::from(pl)
}
}
impl<S> From<Vec<u8>> for Payload<S> {
#[inline]
fn from(vec: Vec<u8>) -> Self {
Payload::from(Bytes::from(vec))
}
}
#[cfg(feature = "http2")]
impl<S> From<crate::h2::Payload> for Payload<S> {
#[inline]
fn from(payload: crate::h2::Payload) -> Self {
Payload::H2 { payload }
}
@ -73,7 +55,6 @@ impl<S> From<crate::h2::Payload> for Payload<S> {
#[cfg(feature = "http2")]
impl<S> From<::h2::RecvStream> for Payload<S> {
#[inline]
fn from(stream: ::h2::RecvStream) -> Self {
Payload::H2 {
payload: crate::h2::Payload::new(stream),
@ -82,15 +63,13 @@ impl<S> From<::h2::RecvStream> for Payload<S> {
}
impl From<BoxedPayloadStream> for Payload {
#[inline]
fn from(payload: BoxedPayloadStream) -> Self {
Payload::Stream { payload }
}
}
impl<S> Payload<S> {
/// Takes current payload and replaces it with `None` value.
#[must_use]
/// Takes current payload and replaces it with `None` value
pub fn take(&mut self) -> Payload<S> {
mem::replace(self, Payload::None)
}

View File

@ -16,10 +16,7 @@ pub struct RequestHead {
pub uri: Uri,
pub version: Version,
pub headers: HeaderMap,
/// Will only be None when called in unit tests unless set manually.
pub peer_addr: Option<net::SocketAddr>,
flags: Flags,
}

View File

@ -173,7 +173,7 @@ impl<P> Request<P> {
/// Peer address is the directly connected peer's socket address. If a proxy is used in front of
/// the Actix Web server, then it would be address of this proxy.
///
/// Will only return None when called in unit tests unless set manually.
/// Will only return None when called in unit tests.
#[inline]
pub fn peer_addr(&self) -> Option<net::SocketAddr> {
self.head().peer_addr

View File

@ -93,7 +93,7 @@ impl ResponseBuilder {
Ok((key, value)) => {
parts.headers.insert(key, value);
}
Err(err) => self.err = Some(err.into()),
Err(e) => self.err = Some(e.into()),
};
}
@ -119,7 +119,7 @@ impl ResponseBuilder {
if let Some(parts) = self.inner() {
match header.try_into_pair() {
Ok((key, value)) => parts.headers.append(key, value),
Err(err) => self.err = Some(err.into()),
Err(e) => self.err = Some(e.into()),
};
}
@ -193,7 +193,7 @@ impl ResponseBuilder {
Ok(value) => {
parts.headers.insert(header::CONTENT_TYPE, value);
}
Err(err) => self.err = Some(err.into()),
Err(e) => self.err = Some(e.into()),
};
}
self
@ -351,9 +351,12 @@ mod tests {
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain");
let resp = Response::build(StatusCode::OK)
.content_type(mime::TEXT_JAVASCRIPT)
.content_type(mime::APPLICATION_JAVASCRIPT_UTF_8)
.body(Bytes::new());
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/javascript");
assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(),
"application/javascript; charset=utf-8"
);
}
#[test]

View File

@ -241,13 +241,13 @@ where
}
/// Configuration options used when accepting TLS connection.
#[cfg(feature = "__tls")]
#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))]
#[derive(Debug, Default)]
pub struct TlsAcceptorConfig {
pub(crate) handshake_timeout: Option<std::time::Duration>,
}
#[cfg(feature = "__tls")]
#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))]
impl TlsAcceptorConfig {
/// Set TLS handshake timeout duration.
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
@ -353,12 +353,12 @@ mod openssl {
}
#[cfg(feature = "rustls-0_20")]
mod rustls_0_20 {
mod rustls_020 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_20::{reexports::ServerConfig, Acceptor, TlsStream},
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
@ -389,7 +389,7 @@ mod rustls_0_20 {
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.20 based service.
/// Create Rustls based service.
pub fn rustls(
self,
config: ServerConfig,
@ -403,7 +403,7 @@ mod rustls_0_20 {
self.rustls_with_config(config, TlsAcceptorConfig::default())
}
/// Create Rustls v0.20 based service with custom TLS acceptor configuration.
/// Create Rustls based service with custom TLS acceptor configuration.
pub fn rustls_with_config(
self,
mut config: ServerConfig,
@ -449,7 +449,7 @@ mod rustls_0_20 {
}
#[cfg(feature = "rustls-0_21")]
mod rustls_0_21 {
mod rustls_021 {
use std::io;
use actix_service::ServiceFactoryExt as _;
@ -485,7 +485,7 @@ mod rustls_0_21 {
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.21 based service.
/// Create Rustls based service.
pub fn rustls_021(
self,
config: ServerConfig,
@ -499,7 +499,7 @@ mod rustls_0_21 {
self.rustls_021_with_config(config, TlsAcceptorConfig::default())
}
/// Create Rustls v0.21 based service with custom TLS acceptor configuration.
/// Create Rustls based service with custom TLS acceptor configuration.
pub fn rustls_021_with_config(
self,
mut config: ServerConfig,
@ -544,198 +544,6 @@ mod rustls_0_21 {
}
}
#[cfg(feature = "rustls-0_22")]
mod rustls_0_22 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.22 based service.
pub fn rustls_0_22(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
self.rustls_0_22_with_config(config, TlsAcceptorConfig::default())
}
/// Create Rustls v0.22 based service with custom TLS acceptor configuration.
pub fn rustls_0_22_with_config(
self,
mut config: ServerConfig,
tls_acceptor_config: TlsAcceptorConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
let mut acceptor = Acceptor::new(config);
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
acceptor.set_handshake_timeout(handshake_timeout);
}
acceptor
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.and_then(|io: TlsStream<TcpStream>| async {
let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2
} else {
Protocol::Http1
}
} else {
Protocol::Http1
};
let peer_addr = io.get_ref().0.peer_addr().ok();
Ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
}
}
}
#[cfg(feature = "rustls-0_23")]
mod rustls_0_23 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.23 based service.
pub fn rustls_0_23(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
self.rustls_0_23_with_config(config, TlsAcceptorConfig::default())
}
/// Create Rustls v0.23 based service with custom TLS acceptor configuration.
pub fn rustls_0_23_with_config(
self,
mut config: ServerConfig,
tls_acceptor_config: TlsAcceptorConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
let mut acceptor = Acceptor::new(config);
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
acceptor.set_handshake_timeout(handshake_timeout);
}
acceptor
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.and_then(|io: TlsStream<TcpStream>| async {
let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2
} else {
Protocol::Http1
}
} else {
Protocol::Http1
};
let peer_addr = io.get_ref().0.peer_addr().ok();
Ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
}
}
}
impl<T, S, B, X, U> ServiceFactory<(T, Protocol, Option<net::SocketAddr>)>
for HttpService<T, S, B, X, U>
where
@ -775,23 +583,23 @@ where
let cfg = self.cfg.clone();
Box::pin(async move {
let expect = expect.await.map_err(|err| {
tracing::error!("Initialization of HTTP expect service error: {err:?}");
})?;
let expect = expect
.await
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
let upgrade = match upgrade {
Some(upgrade) => {
let upgrade = upgrade.await.map_err(|err| {
tracing::error!("Initialization of HTTP upgrade service error: {err:?}");
})?;
let upgrade = upgrade
.await
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
Some(upgrade)
}
None => None,
};
let service = service.await.map_err(|err| {
tracing::error!("Initialization of HTTP service error: {err:?}");
})?;
let service = service
.await
.map_err(|e| error!("Init http service error: {:?}", e))?;
Ok(HttpServiceHandler::new(
cfg,
@ -910,7 +718,7 @@ where
handshake: Some((
crate::h2::handshake_with_timeout(io, &self.cfg),
self.cfg.clone(),
Rc::clone(&self.flow),
self.flow.clone(),
conn_data,
peer_addr,
)),
@ -926,7 +734,7 @@ where
state: State::H1 {
dispatcher: h1::Dispatcher::new(
io,
Rc::clone(&self.flow),
self.flow.clone(),
self.cfg.clone(),
peer_addr,
conn_data,

View File

@ -159,8 +159,8 @@ impl TestBuffer {
#[allow(dead_code)]
pub(crate) fn clone(&self) -> Self {
Self {
read_buf: Rc::clone(&self.read_buf),
write_buf: Rc::clone(&self.write_buf),
read_buf: self.read_buf.clone(),
write_buf: self.write_buf.clone(),
err: self.err.clone(),
}
}

View File

@ -296,7 +296,7 @@ impl Decoder for Codec {
}
}
Ok(None) => Ok(None),
Err(err) => Err(err),
Err(e) => Err(e),
}
}
}

View File

@ -114,14 +114,14 @@ mod inner {
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
DispatcherError::Service(ref err) => {
write!(fmt, "DispatcherError::Service({err:?})")
DispatcherError::Service(ref e) => {
write!(fmt, "DispatcherError::Service({:?})", e)
}
DispatcherError::Encoder(ref err) => {
write!(fmt, "DispatcherError::Encoder({err:?})")
DispatcherError::Encoder(ref e) => {
write!(fmt, "DispatcherError::Encoder({:?})", e)
}
DispatcherError::Decoder(ref err) => {
write!(fmt, "DispatcherError::Decoder({err:?})")
DispatcherError::Decoder(ref e) => {
write!(fmt, "DispatcherError::Decoder({:?})", e)
}
}
}
@ -136,9 +136,9 @@ mod inner {
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
DispatcherError::Service(ref err) => write!(fmt, "{err}"),
DispatcherError::Encoder(ref err) => write!(fmt, "{err:?}"),
DispatcherError::Decoder(ref err) => write!(fmt, "{err:?}"),
DispatcherError::Service(ref e) => write!(fmt, "{}", e),
DispatcherError::Encoder(ref e) => write!(fmt, "{:?}", e),
DispatcherError::Decoder(ref e) => write!(fmt, "{:?}", e),
}
}
}

View File

@ -178,14 +178,14 @@ impl Parser {
};
if payload_len < 126 {
dst.reserve(p_len + 2);
dst.reserve(p_len + 2 + if mask { 4 } else { 0 });
dst.put_slice(&[one, two | payload_len as u8]);
} else if payload_len <= 65_535 {
dst.reserve(p_len + 4);
dst.reserve(p_len + 4 + if mask { 4 } else { 0 });
dst.put_slice(&[one, two | 126]);
dst.put_u16(payload_len as u16);
} else {
dst.reserve(p_len + 10);
dst.reserve(p_len + 10 + if mask { 4 } else { 0 });
dst.put_slice(&[one, two | 127]);
dst.put_u64(payload_len as u64);
};

View File

@ -27,43 +27,43 @@ pub use self::{
#[derive(Debug, Display, Error, From)]
pub enum ProtocolError {
/// Received an unmasked frame from client.
#[display("received an unmasked frame from client")]
#[display(fmt = "received an unmasked frame from client")]
UnmaskedFrame,
/// Received a masked frame from server.
#[display("received a masked frame from server")]
#[display(fmt = "received a masked frame from server")]
MaskedFrame,
/// Encountered invalid opcode.
#[display("invalid opcode ({})", _0)]
#[display(fmt = "invalid opcode ({})", _0)]
InvalidOpcode(#[error(not(source))] u8),
/// Invalid control frame length
#[display("invalid control frame length ({})", _0)]
#[display(fmt = "invalid control frame length ({})", _0)]
InvalidLength(#[error(not(source))] usize),
/// Bad opcode.
#[display("bad opcode")]
#[display(fmt = "bad opcode")]
BadOpCode,
/// A payload reached size limit.
#[display("payload reached size limit")]
#[display(fmt = "payload reached size limit")]
Overflow,
/// Continuation has not started.
#[display("continuation has not started")]
#[display(fmt = "continuation has not started")]
ContinuationNotStarted,
/// Received new continuation but it is already started.
#[display("received new continuation but it has already started")]
#[display(fmt = "received new continuation but it has already started")]
ContinuationStarted,
/// Unknown continuation fragment.
#[display("unknown continuation fragment: {}", _0)]
#[display(fmt = "unknown continuation fragment: {}", _0)]
ContinuationFragment(#[error(not(source))] OpCode),
/// I/O error.
#[display("I/O error: {}", _0)]
#[display(fmt = "I/O error: {}", _0)]
Io(io::Error),
}
@ -71,27 +71,27 @@ pub enum ProtocolError {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
pub enum HandshakeError {
/// Only get method is allowed.
#[display("method not allowed")]
#[display(fmt = "method not allowed")]
GetMethodRequired,
/// Upgrade header if not set to WebSocket.
#[display("WebSocket upgrade is expected")]
#[display(fmt = "WebSocket upgrade is expected")]
NoWebsocketUpgrade,
/// Connection header is not set to upgrade.
#[display("connection upgrade is expected")]
#[display(fmt = "connection upgrade is expected")]
NoConnectionUpgrade,
/// WebSocket version header is not set.
#[display("WebSocket version header is required")]
#[display(fmt = "WebSocket version header is required")]
NoVersionHeader,
/// Unsupported WebSocket version.
#[display("unsupported WebSocket version")]
#[display(fmt = "unsupported WebSocket version")]
UnsupportedVersion,
/// WebSocket key is not set or wrong.
#[display("unknown WebSocket key")]
#[display(fmt = "unknown WebSocket key")]
BadWebsocketKey,
}
@ -221,7 +221,7 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
#[cfg(test)]
mod tests {
use super::*;
use crate::{header, test::TestRequest};
use crate::{header, test::TestRequest, Method};
#[test]
fn test_handshake() {

View File

@ -1,4 +1,7 @@
use std::fmt;
use std::{
convert::{From, Into},
fmt,
};
use base64::prelude::*;
use tracing::error;

View File

@ -94,7 +94,7 @@ async fn with_query_parameter() {
}
#[derive(Debug, Display, Error)]
#[display("expect failed")]
#[display(fmt = "expect failed")]
struct ExpectFailed;
impl From<ExpectFailed> for Response<BoxBody> {

View File

@ -1,4 +1,5 @@
#![cfg(feature = "openssl")]
#![allow(clippy::uninlined_format_args)]
extern crate tls_openssl as openssl;
@ -42,11 +43,9 @@ where
}
fn tls_config() -> SslAcceptor {
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 = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
let cert_file = cert.serialize_pem().unwrap();
let key_file = cert.serialize_private_key_pem();
let cert = X509::from_pem(cert_file.as_bytes()).unwrap();
let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap();
@ -398,7 +397,7 @@ async fn h2_response_http_error_handling() {
}
#[derive(Debug, Display, Error)]
#[display("error")]
#[display(fmt = "error")]
struct BadRequest;
impl From<BadRequest> for Response<BoxBody> {

View File

@ -1,6 +1,6 @@
#![cfg(feature = "rustls-0_23")]
#![cfg(feature = "rustls-0_21")]
extern crate tls_rustls_023 as rustls;
extern crate tls_rustls_021 as rustls;
use std::{
convert::Infallible,
@ -20,13 +20,13 @@ use actix_http::{
use actix_http_test::test_server;
use actix_rt::pin;
use actix_service::{fn_factory_with_config, fn_service};
use actix_tls::connect::rustls_0_23::webpki_roots_cert_store;
use actix_tls::connect::rustls_0_21::webpki_roots_cert_store;
use actix_utils::future::{err, ok, poll_fn};
use bytes::{Bytes, BytesMut};
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::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName};
use rustls_pemfile::{certs, pkcs8_private_keys};
async fn load_body<S>(stream: S) -> Result<BytesMut, PayloadError>
@ -52,25 +52,24 @@ where
}
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 = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
let cert_file = cert.serialize_pem().unwrap();
let key_file = cert.serialize_private_key_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 = certs(cert_file)
.unwrap()
.into_iter()
.map(Certificate)
.collect();
let mut keys = pkcs8_private_keys(key_file).unwrap();
let mut config = RustlsServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(
cert_chain,
rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
)
.with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
.unwrap();
config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec());
@ -84,6 +83,7 @@ pub fn get_negotiated_alpn_protocol(
client_alpn_protocol: &[u8],
) -> Option<Vec<u8>> {
let mut config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(webpki_roots_cert_store())
.with_no_client_auth();
@ -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_021(tls_config())
})
.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_021(tls_config())
})
.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_021(tls_config())
})
.await;
@ -159,7 +159,7 @@ async fn h2_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::ok())
})
.rustls_0_23_with_config(
.rustls_021_with_config(
tls_config(),
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_021(tls_config())
})
.await;
@ -206,7 +206,7 @@ async fn h2_content_length() {
];
ok::<_, Infallible>(Response::new(statuses[indx]))
})
.rustls_0_23(tls_config())
.rustls_021(tls_config())
})
.await;
@ -278,7 +278,7 @@ async fn h2_headers() {
}
ok::<_, Infallible>(config.body(data.clone()))
})
.rustls_0_23(tls_config())
.rustls_021(tls_config())
})
.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_021(tls_config())
})
.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_021(tls_config())
})
.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_021(tls_config())
})
.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_021(tls_config())
})
.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_021(tls_config())
})
.await;
@ -435,7 +435,7 @@ async fn h2_body_chunked_explicit() {
.body(BodyStream::new(body)),
)
})
.rustls_0_23(tls_config())
.rustls_021(tls_config())
})
.await;
@ -464,7 +464,7 @@ async fn h2_response_http_error_handling() {
)
}))
}))
.rustls_0_23(tls_config())
.rustls_021(tls_config())
})
.await;
@ -480,7 +480,7 @@ async fn h2_response_http_error_handling() {
}
#[derive(Debug, Display, Error)]
#[display("error")]
#[display(fmt = "error")]
struct BadRequest;
impl From<BadRequest> for Response<BoxBody> {
@ -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_021(tls_config())
})
.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_021(tls_config())
})
.await;
@ -534,7 +534,7 @@ async fn alpn_h1() -> io::Result<()> {
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok()))
.rustls_0_23(config)
.rustls_021(config)
})
.await;
@ -556,7 +556,7 @@ async fn alpn_h2() -> io::Result<()> {
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok()))
.rustls_0_23(config)
.rustls_021(config)
})
.await;
@ -582,7 +582,7 @@ async fn alpn_h2_1() -> io::Result<()> {
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.finish(|_| ok::<_, Error>(Response::ok()))
.rustls_0_23(config)
.rustls_021(config)
})
.await;

View File

@ -16,7 +16,6 @@ use actix_utils::future::{err, ok, ready};
use bytes::Bytes;
use derive_more::{Display, Error};
use futures_util::{stream::once, FutureExt as _, StreamExt as _};
use rand::Rng as _;
use regex::Regex;
#[actix_rt::test]
@ -63,7 +62,7 @@ async fn h1_2() {
}
#[derive(Debug, Display, Error)]
#[display("expect failed")]
#[display(fmt = "expect failed")]
struct ExpectFailed;
impl From<ExpectFailed> for Response<BoxBody> {
@ -148,7 +147,7 @@ async fn chunked_payload() {
.take_payload()
.map(|res| match res {
Ok(pl) => pl,
Err(err) => panic!("Error reading payload: {err}"),
Err(e) => panic!("Error reading payload: {}", e),
})
.fold(0usize, |acc, chunk| ready(acc + chunk.len()))
.map(|req_size| {
@ -165,10 +164,7 @@ async fn chunked_payload() {
for chunk_size in chunk_sizes.iter() {
let mut bytes = Vec::new();
let random_bytes = rand::rng()
.sample_iter(rand::distr::StandardUniform)
.take(*chunk_size)
.collect::<Vec<_>>();
let random_bytes: Vec<u8> = (0..*chunk_size).map(|_| rand::random::<u8>()).collect();
bytes.extend(format!("{:X}\r\n", chunk_size).as_bytes());
bytes.extend(&random_bytes[..]);
@ -727,7 +723,7 @@ async fn h1_response_http_error_handling() {
}
#[derive(Debug, Display, Error)]
#[display("error")]
#[display(fmt = "error")]
struct BadRequest;
impl From<BadRequest> for Response<BoxBody> {

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::{
cell::Cell,
convert::Infallible,
@ -37,16 +39,16 @@ impl WsService {
#[derive(Debug, Display, Error, From)]
enum WsServiceError {
#[display("HTTP error")]
#[display(fmt = "HTTP error")]
Http(actix_http::Error),
#[display("WS handshake error")]
#[display(fmt = "WS handshake error")]
Ws(actix_http::ws::HandshakeError),
#[display("I/O error")]
#[display(fmt = "I/O error")]
Io(std::io::Error),
#[display("dispatcher error")]
#[display(fmt = "dispatcher error")]
Dispatcher,
}

View File

@ -2,10 +2,6 @@
## Unreleased
## 0.7.0
- Minimum supported Rust version (MSRV) is now 1.72.
## 0.6.1
- Update `syn` dependency to `2`.

View File

@ -1,14 +1,13 @@
[package]
name = "actix-multipart-derive"
version = "0.7.0"
version = "0.6.1"
authors = ["Jacob Halsey <jacob@jhalsey.com>"]
description = "Multipart form derive macro for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"]
homepage.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
license = "MIT OR Apache-2.0"
edition = "2021"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
@ -18,17 +17,14 @@ all-features = true
proc-macro = true
[dependencies]
bytesize = "2"
darling = "0.20"
parse-size = "1"
proc-macro2 = "1"
quote = "1"
syn = "2"
[dev-dependencies]
actix-multipart = "0.7"
actix-multipart = "0.6"
actix-web = "4"
rustversion-msrv = "0.100"
rustversion = "1"
trybuild = "1"
[lints]
workspace = true

View File

@ -1,16 +1,17 @@
# `actix-multipart-derive`
# actix-multipart-derive
> The derive macro implementation for actix-multipart-derive.
<!-- prettier-ignore-start -->
[![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)
[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.5.0)](https://docs.rs/actix-multipart-derive/0.5.0)
![Version](https://img.shields.io/badge/rustc-1.68+-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)
[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.5.0/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.5.0)
[![Download](https://img.shields.io/crates/d/actix-multipart-derive.svg)](https://crates.io/crates/actix-multipart-derive)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end -->
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-multipart-derive)
- Minimum Supported Rust Version (MSRV): 1.68

View File

@ -2,15 +2,16 @@
//!
//! See [`macro@MultipartForm`] for usage examples.
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![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))]
#![allow(clippy::disallowed_names)] // false positives in some macro expansions
use std::collections::HashSet;
use bytesize::ByteSize;
use darling::{FromDeriveInput, FromField, FromMeta};
use parse_size::parse_size;
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
@ -36,7 +37,6 @@ struct MultipartFormAttrs {
duplicate_field: DuplicateField,
}
#[allow(clippy::disallowed_names)] // false positive in macro expansion
#[derive(FromField, Default)]
#[darling(attributes(multipart), default)]
struct FieldAttrs {
@ -103,7 +103,7 @@ struct ParsedField<'t> {
/// # Field Limits
///
/// You can use the `#[multipart(limit = "<size>")]` attribute to set field level limits. The limit
/// string is parsed using [`bytesize`].
/// string is parsed using [parse_size].
///
/// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
///
@ -138,7 +138,7 @@ struct ParsedField<'t> {
/// `#[multipart(duplicate_field = "<behavior>")]` attribute:
///
/// - "ignore": (default) Extra fields are ignored. I.e., the first one is persisted.
/// - "deny": A `MultipartError::UnknownField` error response is returned.
/// - "deny": A `MultipartError::UnsupportedField` error response is returned.
/// - "replace": Each field is processed, but only the last one is persisted.
///
/// Note that `Vec` fields will ignore this option.
@ -150,7 +150,7 @@ struct ParsedField<'t> {
/// struct Form { }
/// ```
///
/// [`bytesize`]: https://docs.rs/bytesize/2
/// [parse_size]: https://docs.rs/parse-size/1/parse_size
#[proc_macro_derive(MultipartForm, attributes(multipart))]
pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: syn::DeriveInput = parse_macro_input!(input);
@ -191,8 +191,8 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
let attrs = FieldAttrs::from_field(field).map_err(|err| err.write_errors())?;
let serialization_name = attrs.rename.unwrap_or_else(|| rust_name.to_string());
let limit = match attrs.limit.map(|limit| match limit.parse::<ByteSize>() {
Ok(ByteSize(size)) => Ok(usize::try_from(size).unwrap()),
let limit = match attrs.limit.map(|limit| match parse_size(&limit) {
Ok(size) => Ok(usize::try_from(size).unwrap()),
Err(err) => Err(syn::Error::new(
field.ident.as_ref().unwrap().span(),
format!("Could not parse size limit `{}`: {}", limit, err),
@ -229,7 +229,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
// Return value when a field name is not supported by the form
let unknown_field_result = if attrs.deny_unknown_fields {
quote!(::std::result::Result::Err(
::actix_multipart::MultipartError::UnknownField(field.name().unwrap().to_string())
::actix_multipart::MultipartError::UnsupportedField(field.name().to_string())
))
} else {
quote!(::std::result::Result::Ok(()))
@ -292,7 +292,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
limits: &'t mut ::actix_multipart::form::Limits,
state: &'t mut ::actix_multipart::form::State,
) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::actix_multipart::MultipartError>> + 't>> {
match field.name().unwrap() {
match field.name() {
#handle_field_impl
_ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)),
}

View File

@ -1,4 +1,4 @@
#[rustversion_msrv::msrv]
#[rustversion::stable(1.68)] // MSRV
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();

View File

@ -1,16 +1,16 @@
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`: invalid digit found in string
--> 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`: invalid digit found in string
--> tests/trybuild/size-limit-parse-fail.rs:12:5
|
12 | description: Text<String>,
| ^^^^^^^^^^^
error: Could not parse size limit `four meters`: couldn't parse "four meters" into a ByteSize, cannot parse float from empty string
error: Could not parse size limit `four meters`: invalid digit found in string
--> tests/trybuild/size-limit-parse-fail.rs:18:5
|
18 | description: Text<String>,

View File

@ -2,33 +2,6 @@
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.7.2
- Fix re-exported version of `actix-multipart-derive`.
## 0.7.1
- Expose `LimitExceeded` error type.
## 0.7.0
- Add `MultipartError::ContentTypeIncompatible` variant.
- Add `MultipartError::ContentDispositionNameMissing` variant.
- Add `Field::bytes()` method.
- Rename `MultipartError::{NoContentDisposition => ContentDispositionMissing}` variant.
- Rename `MultipartError::{NoContentType => ContentTypeMissing}` variant.
- Rename `MultipartError::{ParseContentType => ContentTypeParse}` variant.
- Rename `MultipartError::{Boundary => BoundaryMissing}` variant.
- Rename `MultipartError::{UnsupportedField => UnknownField}` variant.
- Remove top-level re-exports of `test` utilities.
## 0.6.2
- Add testing utilities under new module `test`.
- Minimum supported Rust version (MSRV) is now 1.72.
## 0.6.1
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.

View File

@ -1,48 +1,33 @@
[package]
name = "actix-multipart"
version = "0.7.2"
version = "0.6.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Jacob Halsey <jacob@jhalsey.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Multipart request & form support for Actix Web"
keywords = ["http", "actix", "web", "multipart", "form"]
homepage.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
license = "MIT OR Apache-2.0"
edition = "2021"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[package.metadata.cargo_check_external_types]
allowed_external_types = [
"actix_http::*",
"actix_multipart_derive::*",
"actix_utils::*",
"actix_web::*",
"bytes::*",
"futures_core::*",
"mime::*",
"serde_json::*",
"serde_plain::*",
"serde::*",
"tempfile::*",
]
[features]
default = ["tempfile", "derive"]
derive = ["actix-multipart-derive"]
tempfile = ["dep:tempfile", "tokio/fs"]
[dependencies]
actix-multipart-derive = { version = "=0.7.0", optional = true }
actix-multipart-derive = { version = "=0.6.1", optional = true }
actix-utils = "3"
actix-web = { version = "4", default-features = false }
derive_more = { version = "2", features = ["display", "error", "from"] }
bytes = "1"
derive_more = "0.99.5"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
httparse = "1.3"
@ -50,7 +35,6 @@ local-waker = "0.1"
log = "0.4"
memchr = "2.5"
mime = "0.3"
rand = "0.9"
serde = "1"
serde_json = "1"
serde_plain = "1"
@ -59,18 +43,10 @@ tokio = { version = "1.24.2", features = ["sync", "io-util"] }
[dev-dependencies]
actix-http = "3"
actix-multipart-rfc7578 = "0.11"
actix-multipart-rfc7578 = "0.10"
actix-rt = "2.2"
actix-test = "0.1"
actix-web = "4"
assert_matches = "1"
awc = "3"
env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-test = "0.3"
multer = "3"
tokio = { version = "1.24.2", features = ["sync"] }
tokio-stream = "0.1"
[lints]
workspace = true

View File

@ -1,74 +1,17 @@
# `actix-multipart`
# actix-multipart
<!-- prettier-ignore-start -->
> Multipart form support for Actix Web.
[![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)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.6.1)](https://docs.rs/actix-multipart/0.6.1)
![Version](https://img.shields.io/badge/rustc-1.68+-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)
[![dependency status](https://deps.rs/crate/actix-multipart/0.6.1/status.svg)](https://deps.rs/crate/actix-multipart/0.6.1)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end -->
## Documentation & Resources
<!-- cargo-rdme start -->
Multipart request & form support for Actix Web.
The [`Multipart`] extractor aims to support all kinds of `multipart/*` requests, including `multipart/form-data`, `multipart/related` and `multipart/mixed`. This is a lower-level extractor which supports reading [multipart fields](Field), in the order they are sent by the client.
Due to additional requirements for `multipart/form-data` requests, the higher level [`MultipartForm`] extractor and derive macro only supports this media type.
## Examples
```rust
use actix_web::{post, App, HttpServer, Responder};
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Metadata {
name: String,
}
#[derive(Debug, MultipartForm)]
struct UploadForm {
#[multipart(limit = "100MB")]
file: TempFile,
json: MpJson<Metadata>,
}
#[post("/videos")]
pub async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
format!(
"Uploaded file {}, with size: {}",
form.json.name, form.file.size
)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(move || App::new().service(post_video))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
```
cURL request:
```sh
curl -v --request POST \
--url http://localhost:8080/videos \
-F 'json={"name": "Cargo.lock"};type=application/json' \
-F file=@./Cargo.lock
```
[`MultipartForm`]: struct@form::MultipartForm
<!-- cargo-rdme end -->
[More available in the examples repo &rarr;](https://github.com/actix/examples/tree/master/forms/multipart)
- [API Documentation](https://docs.rs/actix-multipart)
- Minimum Supported Rust Version (MSRV): 1.68

View File

@ -1,36 +0,0 @@
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
use actix_web::{middleware::Logger, post, App, HttpServer, Responder};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Metadata {
name: String,
}
#[derive(Debug, MultipartForm)]
struct UploadForm {
#[multipart(limit = "100MB")]
file: TempFile,
json: MpJson<Metadata>,
}
#[post("/videos")]
async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
format!(
"Uploaded file {}, with size: {}\ntemporary file ({}) was deleted\n",
form.json.name,
form.file.size,
form.file.file.path().display(),
)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
HttpServer::new(move || App::new().service(post_video).wrap(Logger::default()))
.workers(2)
.bind(("127.0.0.1", 8080))?
.run()
.await
}

View File

@ -10,96 +10,78 @@ use derive_more::{Display, Error, From};
/// A set of errors that can occur during parsing multipart streams.
#[derive(Debug, Display, From, Error)]
#[non_exhaustive]
pub enum Error {
/// Could not find Content-Type header.
#[display("Could not find Content-Type header")]
ContentTypeMissing,
/// Could not parse Content-Type header.
#[display("Could not parse Content-Type header")]
ContentTypeParse,
/// Parsed Content-Type did not have "multipart" top-level media type.
pub enum MultipartError {
/// Content-Disposition header is not found or is not equal to "form-data".
///
/// Also raised when extracting a [`MultipartForm`] from a request that does not have the
/// "multipart/form-data" media type.
///
/// [`MultipartForm`]: struct@crate::form::MultipartForm
#[display("Parsed Content-Type did not have 'multipart' top-level media type")]
ContentTypeIncompatible,
/// According to [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) a
/// Content-Disposition header must always be present and equal to "form-data".
#[display(fmt = "No Content-Disposition `form-data` header")]
NoContentDisposition,
/// Multipart boundary is not found.
#[display("Multipart boundary is not found")]
BoundaryMissing,
/// Content-Type header is not found
#[display(fmt = "No Content-Type header found")]
NoContentType,
/// Content-Disposition header was not found or not of disposition type "form-data" when parsing
/// a "form-data" field.
///
/// As per [RFC 7578 §4.2], a "multipart/form-data" field's Content-Disposition header must
/// always be present and have a disposition type of "form-data".
///
/// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
#[display("Content-Disposition header was not found when parsing a \"form-data\" field")]
ContentDispositionMissing,
/// Can not parse Content-Type header
#[display(fmt = "Can not parse Content-Type header")]
ParseContentType,
/// Content-Disposition name parameter was not found when parsing a "form-data" field.
///
/// As per [RFC 7578 §4.2], a "multipart/form-data" field's Content-Disposition header must
/// always include a "name" parameter.
///
/// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
#[display("Content-Disposition header was not found when parsing a \"form-data\" field")]
ContentDispositionNameMissing,
/// Multipart boundary is not found
#[display(fmt = "Multipart boundary is not found")]
Boundary,
/// Nested multipart is not supported.
#[display("Nested multipart is not supported")]
/// Nested multipart is not supported
#[display(fmt = "Nested multipart is not supported")]
Nested,
/// Multipart stream is incomplete.
#[display("Multipart stream is incomplete")]
/// Multipart stream is incomplete
#[display(fmt = "Multipart stream is incomplete")]
Incomplete,
/// Field parsing failed.
#[display("Error during field parsing")]
/// Error during field parsing
#[display(fmt = "{}", _0)]
Parse(ParseError),
/// HTTP payload error.
#[display("Payload error")]
/// Payload error
#[display(fmt = "{}", _0)]
Payload(PayloadError),
/// Stream is not consumed.
#[display("Stream is not consumed")]
/// Not consumed
#[display(fmt = "Multipart stream is not consumed")]
NotConsumed,
/// Form field handler raised error.
#[display("An error occurred processing field: {name}")]
/// An error from a field handler in a form
#[display(
fmt = "An error occurred processing field `{}`: {}",
field_name,
source
)]
Field {
name: String,
field_name: String,
source: actix_web::Error,
},
/// Duplicate field found (for structure that opted-in to denying duplicate fields).
#[display("Duplicate field found: {_0}")]
/// Duplicate field
#[display(fmt = "Duplicate field found for: `{}`", _0)]
#[from(ignore)]
DuplicateField(#[error(not(source))] String),
/// Required field is missing.
#[display("Required field is missing: {_0}")]
/// Missing field
#[display(fmt = "Field with name `{}` is required", _0)]
#[from(ignore)]
MissingField(#[error(not(source))] String),
/// Unknown field (for structure that opted-in to denying unknown fields).
#[display("Unknown field: {_0}")]
/// Unknown field
#[display(fmt = "Unsupported field `{}`", _0)]
#[from(ignore)]
UnknownField(#[error(not(source))] String),
UnsupportedField(#[error(not(source))] String),
}
/// Return `BadRequest` for `MultipartError`.
impl ResponseError for Error {
/// Return `BadRequest` for `MultipartError`
impl ResponseError for MultipartError {
fn status_code(&self) -> StatusCode {
match &self {
Error::Field { source, .. } => source.as_response_error().status_code(),
Error::ContentTypeIncompatible => StatusCode::UNSUPPORTED_MEDIA_TYPE,
MultipartError::Field { source, .. } => source.as_response_error().status_code(),
_ => StatusCode::BAD_REQUEST,
}
}
@ -111,7 +93,7 @@ mod tests {
#[test]
fn test_multipart_error() {
let resp = Error::BoundaryMissing.error_response();
let resp = MultipartError::Boundary.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
}

View File

@ -1,20 +1,21 @@
//! Multipart payload support
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
use crate::multipart::Multipart;
use crate::server::Multipart;
/// Extract request's payload as multipart stream.
/// Get request's payload as multipart stream.
///
/// Content-type: multipart/*;
/// Content-type: multipart/form-data;
///
/// # Examples
///
/// ```
/// use actix_web::{web, HttpResponse};
/// use actix_web::{web, HttpResponse, Error};
/// use actix_multipart::Multipart;
/// use futures_util::StreamExt as _;
///
/// async fn index(mut payload: Multipart) -> actix_web::Result<HttpResponse> {
/// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> {
/// // iterate over multipart stream
/// while let Some(item) = payload.next().await {
/// let mut field = item?;
@ -25,7 +26,7 @@ use crate::multipart::Multipart;
/// }
/// }
///
/// Ok(HttpResponse::Ok().finish())
/// Ok(HttpResponse::Ok().into())
/// }
/// ```
impl FromRequest for Multipart {
@ -34,6 +35,9 @@ impl FromRequest for Multipart {
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
ready(Ok(Multipart::from_req(req, payload)))
ready(Ok(match Multipart::boundary(req.headers()) {
Ok(boundary) => Multipart::from_boundary(boundary, payload.take()),
Err(err) => Multipart::from_error(err),
}))
}
}

View File

@ -1,501 +0,0 @@
use std::{
cell::RefCell,
cmp, fmt,
future::poll_fn,
mem,
pin::Pin,
rc::Rc,
task::{ready, Context, Poll},
};
use actix_web::{
error::PayloadError,
http::header::{self, ContentDisposition, HeaderMap},
web::{Bytes, BytesMut},
};
use derive_more::{Display, Error};
use futures_core::Stream;
use mime::Mime;
use crate::{
error::Error,
payload::{PayloadBuffer, PayloadRef},
safety::Safety,
};
/// Error type returned from [`Field::bytes()`] when field data is larger than limit.
#[derive(Debug, Display, Error)]
#[display("size limit exceeded while collecting field data")]
#[non_exhaustive]
pub struct LimitExceeded;
/// A single field in a multipart stream.
pub struct Field {
/// Field's Content-Type.
content_type: Option<Mime>,
/// Field's Content-Disposition.
content_disposition: Option<ContentDisposition>,
/// Form field name.
///
/// A non-optional storage for form field names to avoid unwraps in `form` module. Will be an
/// empty string in non-form contexts.
///
// INVARIANT: always non-empty when request content-type is multipart/form-data.
pub(crate) form_field_name: String,
/// Field's header map.
headers: HeaderMap,
safety: Safety,
inner: Rc<RefCell<InnerField>>,
}
impl Field {
pub(crate) fn new(
content_type: Option<Mime>,
content_disposition: Option<ContentDisposition>,
form_field_name: Option<String>,
headers: HeaderMap,
safety: Safety,
inner: Rc<RefCell<InnerField>>,
) -> Self {
Field {
content_type,
content_disposition,
form_field_name: form_field_name.unwrap_or_default(),
headers,
inner,
safety,
}
}
/// Returns a reference to the field's header map.
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
/// Returns a reference to the field's content (mime) type, if it is supplied by the client.
///
/// According to [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578#section-4.4), if it is not
/// present, it should default to "text/plain". Note it is the responsibility of the client to
/// provide the appropriate content type, there is no attempt to validate this by the server.
pub fn content_type(&self) -> Option<&Mime> {
self.content_type.as_ref()
}
/// Returns this field's parsed Content-Disposition header, if set.
///
/// # Validation
///
/// Per [RFC 7578 §4.2], the parts of a multipart/form-data payload MUST contain a
/// Content-Disposition header field where the disposition type is `form-data` and MUST also
/// contain an additional parameter of `name` with its value being the original field name from
/// the form. This requirement is enforced during extraction for multipart/form-data requests,
/// but not other kinds of multipart requests (such as multipart/related).
///
/// As such, it is safe to `.unwrap()` calls `.content_disposition()` if you've verified.
///
/// The [`name()`](Self::name) method is also provided as a convenience for obtaining the
/// aforementioned name parameter.
///
/// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
pub fn content_disposition(&self) -> Option<&ContentDisposition> {
self.content_disposition.as_ref()
}
/// Returns the field's name, if set.
///
/// See [`content_disposition()`](Self::content_disposition) regarding guarantees on presence of
/// the "name" field.
pub fn name(&self) -> Option<&str> {
self.content_disposition()?.get_name()
}
/// Collects the raw field data, up to `limit` bytes.
///
/// # Errors
///
/// Any errors produced by the data stream are returned as `Ok(Err(Error))` immediately.
///
/// If the buffered data size would exceed `limit`, an `Err(LimitExceeded)` is returned. Note
/// that, in this case, the full data stream is exhausted before returning the error so that
/// subsequent fields can still be read. To better defend against malicious/infinite requests,
/// it is advisable to also put a timeout on this call.
pub async fn bytes(&mut self, limit: usize) -> Result<Result<Bytes, Error>, LimitExceeded> {
/// Sensible default (2kB) for initial, bounded allocation when collecting body bytes.
const INITIAL_ALLOC_BYTES: usize = 2 * 1024;
let mut exceeded_limit = false;
let mut buf = BytesMut::with_capacity(INITIAL_ALLOC_BYTES);
let mut field = Pin::new(self);
match poll_fn(|cx| loop {
match ready!(field.as_mut().poll_next(cx)) {
// if already over limit, discard chunk to advance multipart request
Some(Ok(_chunk)) if exceeded_limit => {}
// if limit is exceeded set flag to true and continue
Some(Ok(chunk)) if buf.len() + chunk.len() > limit => {
exceeded_limit = true;
// eagerly de-allocate field data buffer
let _ = mem::take(&mut buf);
}
Some(Ok(chunk)) => buf.extend_from_slice(&chunk),
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await
{
// propagate error returned from body poll
Err(err) => Ok(Err(err)),
// limit was exceeded while reading body
Ok(()) if exceeded_limit => Err(LimitExceeded),
// otherwise return body buffer
Ok(()) => Ok(Ok(buf.freeze())),
}
}
}
impl Stream for Field {
type Item = Result<Bytes, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
let mut inner = this.inner.borrow_mut();
if let Some(mut buffer) = inner
.payload
.as_ref()
.expect("Field should not be polled after completion")
.get_mut(&this.safety)
{
// check safety and poll read payload to buffer.
buffer.poll_stream(cx)?;
} else if !this.safety.is_clean() {
// safety violation
return Poll::Ready(Some(Err(Error::NotConsumed)));
} else {
return Poll::Pending;
}
inner.poll(&this.safety)
}
}
impl fmt::Debug for Field {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ct) = &self.content_type {
writeln!(f, "\nField: {}", ct)?;
} else {
writeln!(f, "\nField:")?;
}
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
writeln!(f, " headers:")?;
for (key, val) in self.headers.iter() {
writeln!(f, " {:?}: {:?}", key, val)?;
}
Ok(())
}
}
pub(crate) struct InnerField {
/// Payload is initialized as Some and is `take`n when the field stream finishes.
payload: Option<PayloadRef>,
/// Field boundary (without "--" prefix).
boundary: String,
/// True if request payload has been exhausted.
eof: bool,
/// Field data's stated size according to it's Content-Length header.
length: Option<u64>,
}
impl InnerField {
pub(crate) fn new_in_rc(
payload: PayloadRef,
boundary: String,
headers: &HeaderMap,
) -> Result<Rc<RefCell<InnerField>>, PayloadError> {
Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this)))
}
pub(crate) fn new(
payload: PayloadRef,
boundary: String,
headers: &HeaderMap,
) -> Result<InnerField, PayloadError> {
let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) {
match len.to_str().ok().and_then(|len| len.parse::<u64>().ok()) {
Some(len) => Some(len),
None => return Err(PayloadError::Incomplete(None)),
}
} else {
None
};
Ok(InnerField {
boundary,
payload: Some(payload),
eof: false,
length: len,
})
}
/// Reads body part content chunk of the specified size.
///
/// The body part must has `Content-Length` header with proper value.
pub(crate) fn read_len(
payload: &mut PayloadBuffer,
size: &mut u64,
) -> Poll<Option<Result<Bytes, Error>>> {
if *size == 0 {
Poll::Ready(None)
} else {
match payload.read_max(*size)? {
Some(mut chunk) => {
let len = cmp::min(chunk.len() as u64, *size);
*size -= len;
let ch = chunk.split_to(len as usize);
if !chunk.is_empty() {
payload.unprocessed(chunk);
}
Poll::Ready(Some(Ok(ch)))
}
None => {
if payload.eof && (*size != 0) {
Poll::Ready(Some(Err(Error::Incomplete)))
} else {
Poll::Pending
}
}
}
}
}
/// Reads content chunk of body part with unknown length.
///
/// The `Content-Length` header for body part is not necessary.
pub(crate) fn read_stream(
payload: &mut PayloadBuffer,
boundary: &str,
) -> Poll<Option<Result<Bytes, Error>>> {
let mut pos = 0;
let len = payload.buf.len();
if len == 0 {
return if payload.eof {
Poll::Ready(Some(Err(Error::Incomplete)))
} else {
Poll::Pending
};
}
// check boundary
if len > 4 && payload.buf[0] == b'\r' {
let b_len = if payload.buf.starts_with(b"\r\n") && &payload.buf[2..4] == b"--" {
Some(4)
} else if &payload.buf[1..3] == b"--" {
Some(3)
} else {
None
};
if let Some(b_len) = b_len {
let b_size = boundary.len() + b_len;
if len < b_size {
return Poll::Pending;
} else if &payload.buf[b_len..b_size] == boundary.as_bytes() {
// found boundary
return Poll::Ready(None);
}
}
}
loop {
return if let Some(idx) = memchr::memmem::find(&payload.buf[pos..], b"\r") {
let cur = pos + idx;
// check if we have enough data for boundary detection
if cur + 4 > len {
if cur > 0 {
Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze())))
} else {
Poll::Pending
}
} else {
// check boundary
if (&payload.buf[cur..cur + 2] == b"\r\n"
&& &payload.buf[cur + 2..cur + 4] == b"--")
|| (&payload.buf[cur..=cur] == b"\r"
&& &payload.buf[cur + 1..cur + 3] == b"--")
{
if cur != 0 {
// return buffer
Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze())))
} else {
pos = cur + 1;
continue;
}
} else {
// not boundary
pos = cur + 1;
continue;
}
}
} else {
Poll::Ready(Some(Ok(payload.buf.split().freeze())))
};
}
}
pub(crate) fn poll(&mut self, safety: &Safety) -> Poll<Option<Result<Bytes, Error>>> {
if self.payload.is_none() {
return Poll::Ready(None);
}
let Some(mut payload) = self
.payload
.as_ref()
.expect("Field should not be polled after completion")
.get_mut(safety)
else {
return Poll::Pending;
};
if !self.eof {
let res = if let Some(ref mut len) = self.length {
Self::read_len(&mut payload, len)
} else {
Self::read_stream(&mut payload, &self.boundary)
};
match ready!(res) {
Some(Ok(bytes)) => return Poll::Ready(Some(Ok(bytes))),
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
None => self.eof = true,
}
}
let result = match payload.readline() {
Ok(None) => Poll::Pending,
Ok(Some(line)) => {
if line.as_ref() != b"\r\n" {
log::warn!("multipart field did not read all the data or it is malformed");
}
Poll::Ready(None)
}
Err(err) => Poll::Ready(Some(Err(err))),
};
drop(payload);
if let Poll::Ready(None) = result {
// drop payload buffer and make future un-poll-able
let _ = self.payload.take();
}
result
}
}
#[cfg(test)]
mod tests {
use futures_util::{stream, StreamExt as _};
use super::*;
use crate::Multipart;
// TODO: use test utility when multi-file support is introduced
fn create_double_request_with_header() -> (Bytes, HeaderMap) {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
\r\n\
one+one+one\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
\r\n\
two+two+two\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
(bytes, headers)
}
#[actix_rt::test]
async fn bytes_unlimited() {
let (body, headers) = create_double_request_with_header();
let mut multipart = Multipart::new(&headers, stream::iter([Ok(body)]));
let field = multipart
.next()
.await
.expect("multipart should have two fields")
.expect("multipart body should be well formatted")
.bytes(usize::MAX)
.await
.expect("field data should not be size limited")
.expect("reading field data should not error");
assert_eq!(field, "one+one+one");
let field = multipart
.next()
.await
.expect("multipart should have two fields")
.expect("multipart body should be well formatted")
.bytes(usize::MAX)
.await
.expect("field data should not be size limited")
.expect("reading field data should not error");
assert_eq!(field, "two+two+two");
}
#[actix_rt::test]
async fn bytes_limited() {
let (body, headers) = create_double_request_with_header();
let mut multipart = Multipart::new(&headers, stream::iter([Ok(body)]));
multipart
.next()
.await
.expect("multipart should have two fields")
.expect("multipart body should be well formatted")
.bytes(8) // smaller than data size
.await
.expect_err("field data should be size limited");
// next field still readable
let field = multipart
.next()
.await
.expect("multipart should have two fields")
.expect("multipart body should be well formatted")
.bytes(usize::MAX)
.await
.expect("field data should not be size limited")
.expect("reading field data should not error");
assert_eq!(field, "two+two+two");
}
}

View File

@ -1,6 +1,7 @@
//! Reads a field into memory.
use actix_web::{web::BytesMut, HttpRequest};
use actix_web::HttpRequest;
use bytes::BytesMut;
use futures_core::future::LocalBoxFuture;
use futures_util::TryStreamExt as _;
use mime::Mime;
@ -14,7 +15,7 @@ use crate::{
#[derive(Debug)]
pub struct Bytes {
/// The data.
pub data: actix_web::web::Bytes,
pub data: bytes::Bytes,
/// The value of the `Content-Type` header.
pub content_type: Option<Mime>,
@ -40,9 +41,8 @@ impl<'t> FieldReader<'t> for Bytes {
content_type: field.content_type().map(ToOwned::to_owned),
file_name: field
.content_disposition()
.expect("multipart form fields should have a content-disposition header")
.get_filename()
.map(ToOwned::to_owned),
.map(str::to_owned),
})
})
}

View File

@ -32,6 +32,7 @@ where
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future {
Box::pin(async move {
let config = JsonConfig::from_req(req);
let field_name = field.name().to_owned();
if config.validate_content_type {
let valid = if let Some(mime) = field.content_type() {
@ -42,19 +43,17 @@ where
if !valid {
return Err(MultipartError::Field {
name: field.form_field_name,
field_name,
source: config.map_error(req, JsonFieldError::ContentType),
});
}
}
let form_field_name = field.form_field_name.clone();
let bytes = Bytes::read_field(req, field, limits).await?;
Ok(Json(serde_json::from_slice(bytes.data.as_ref()).map_err(
|err| MultipartError::Field {
name: form_field_name,
field_name,
source: config.map_error(req, JsonFieldError::Deserialize(err)),
},
)?))
@ -66,11 +65,11 @@ where
#[non_exhaustive]
pub enum JsonFieldError {
/// Deserialize error.
#[display("Json deserialize error: {}", _0)]
#[display(fmt = "Json deserialize error: {}", _0)]
Deserialize(serde_json::Error),
/// Content type error.
#[display("Content type error")]
#[display(fmt = "Content type error")]
ContentType,
}
@ -132,12 +131,14 @@ impl Default for JsonConfig {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::{collections::HashMap, io::Cursor};
use actix_web::{http::StatusCode, web, web::Bytes, App, HttpResponse, Responder};
use actix_multipart_rfc7578::client::multipart;
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
use crate::form::{
json::{Json, JsonConfig},
tests::send_form,
MultipartForm,
};
@ -154,8 +155,6 @@ mod tests {
HttpResponse::Ok().finish()
}
const TEST_JSON: &str = r#"{"key1": "value1", "key2": "value2"}"#;
#[actix_rt::test]
async fn test_json_without_content_type() {
let srv = actix_test::start(|| {
@ -164,16 +163,10 @@ mod tests {
.app_data(JsonConfig::default().validate_content_type(false))
});
let (body, headers) = crate::test::create_form_data_payload_and_headers(
"json",
None,
None,
Bytes::from_static(TEST_JSON.as_bytes()),
);
let mut req = srv.post("/");
*req.headers_mut() = headers;
let res = req.send_body(body).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let mut form = multipart::Form::default();
form.add_text("json", "{\"key1\": \"value1\", \"key2\": \"value2\"}");
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_rt::test]
@ -185,27 +178,17 @@ mod tests {
});
// Deny because wrong content type
let (body, headers) = crate::test::create_form_data_payload_and_headers(
"json",
None,
Some(mime::APPLICATION_OCTET_STREAM),
Bytes::from_static(TEST_JSON.as_bytes()),
);
let mut req = srv.post("/");
*req.headers_mut() = headers;
let res = req.send_body(body).await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
let mut form = multipart::Form::default();
form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_OCTET_STREAM);
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
// Allow because correct content type
let (body, headers) = crate::test::create_form_data_payload_and_headers(
"json",
None,
Some(mime::APPLICATION_JSON),
Bytes::from_static(TEST_JSON.as_bytes()),
);
let mut req = srv.post("/");
*req.headers_mut() = headers;
let res = req.send_body(body).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
let mut form = multipart::Form::default();
form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_JSON);
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
}
}

View File

@ -1,4 +1,4 @@
//! Extract and process typed data from fields of a `multipart/form-data` request.
//! Process and extract typed data from a multipart stream.
use std::{
any::Any,
@ -33,14 +33,6 @@ pub trait FieldReader<'t>: Sized + Any {
type Future: Future<Output = Result<Self, MultipartError>>;
/// The form will call this function to handle the field.
///
/// # Panics
///
/// When reading the `field` payload using its `Stream` implementation, polling (manually or via
/// `next()`/`try_next()`) may panic after the payload is exhausted. If this is a problem for
/// your implementation of this method, you should [`fuse()`] the `Field` first.
///
/// [`fuse()`]: futures_util::stream::StreamExt::fuse()
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future;
}
@ -80,13 +72,13 @@ where
state: &'t mut State,
duplicate_field: DuplicateField,
) -> Self::Future {
if state.contains_key(&field.form_field_name) {
if state.contains_key(field.name()) {
match duplicate_field {
DuplicateField::Ignore => return Box::pin(ready(Ok(()))),
DuplicateField::Deny => {
return Box::pin(ready(Err(MultipartError::DuplicateField(
field.form_field_name,
field.name().to_owned(),
))))
}
@ -95,7 +87,7 @@ where
}
Box::pin(async move {
let field_name = field.form_field_name.clone();
let field_name = field.name().to_owned();
let t = T::read_field(req, field, limits).await?;
state.insert(field_name, Box::new(t));
Ok(())
@ -123,8 +115,10 @@ where
Box::pin(async move {
// Note: Vec GroupReader always allows duplicates
let field_name = field.name().to_owned();
let vec = state
.entry(field.form_field_name.clone())
.entry(field_name)
.or_insert_with(|| Box::<Vec<T>>::default())
.downcast_mut::<Vec<T>>()
.unwrap();
@ -157,13 +151,13 @@ where
state: &'t mut State,
duplicate_field: DuplicateField,
) -> Self::Future {
if state.contains_key(&field.form_field_name) {
if state.contains_key(field.name()) {
match duplicate_field {
DuplicateField::Ignore => return Box::pin(ready(Ok(()))),
DuplicateField::Deny => {
return Box::pin(ready(Err(MultipartError::DuplicateField(
field.form_field_name,
field.name().to_owned(),
))))
}
@ -172,7 +166,7 @@ where
}
Box::pin(async move {
let field_name = field.form_field_name.clone();
let field_name = field.name().to_owned();
let t = T::read_field(req, field, limits).await?;
state.insert(field_name, Box::new(t));
Ok(())
@ -279,9 +273,6 @@ impl Limits {
/// [`MultipartCollect`] trait. You should use the [`macro@MultipartForm`] macro to derive this
/// for your struct.
///
/// Note that this extractor rejects requests with any other Content-Type such as `multipart/mixed`,
/// `multipart/related`, or non-multipart media types.
///
/// Add a [`MultipartFormConfig`] to your app data to configure extraction.
#[derive(Deref, DerefMut)]
pub struct MultipartForm<T: MultipartCollect>(pub T);
@ -295,24 +286,14 @@ impl<T: MultipartCollect> MultipartForm<T> {
impl<T> FromRequest for MultipartForm<T>
where
T: MultipartCollect + 'static,
T: MultipartCollect,
{
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
let mut multipart = Multipart::from_req(req, payload);
let content_type = match multipart.content_type_or_bail() {
Ok(content_type) => content_type,
Err(err) => return Box::pin(ready(Err(err.into()))),
};
if content_type.subtype() != mime::FORM_DATA {
// this extractor only supports multipart/form-data
return Box::pin(ready(Err(MultipartError::ContentTypeIncompatible.into())));
};
let mut payload = Multipart::new(req.headers(), payload.take());
let config = MultipartFormConfig::from_req(req);
let mut limits = Limits::new(config.total_limit, config.memory_limit);
@ -324,29 +305,21 @@ where
Box::pin(
async move {
let mut state = State::default();
// ensure limits are shared for all fields with this name
// We need to ensure field limits are shared for all instances of this field name
let mut field_limits = HashMap::<String, Option<usize>>::new();
while let Some(field) = multipart.try_next().await? {
debug_assert!(
!field.form_field_name.is_empty(),
"multipart form fields should have names",
);
while let Some(field) = payload.try_next().await? {
// Retrieve the limit for this field
let entry = field_limits
.entry(field.form_field_name.clone())
.or_insert_with(|| T::limit(&field.form_field_name));
limits.field_limit_remaining.clone_from(entry);
.entry(field.name().to_owned())
.or_insert_with(|| T::limit(field.name()));
limits.field_limit_remaining = entry.to_owned();
T::handle_field(&req, field, &mut limits, &mut state).await?;
// Update the stored limit
*entry = limits.field_limit_remaining;
}
let inner = T::from_state(state)?;
Ok(MultipartForm(inner))
}
@ -422,20 +395,11 @@ mod tests {
use actix_http::encoding::Decoder;
use actix_multipart_rfc7578::client::multipart;
use actix_test::TestServer;
use actix_web::{
dev::Payload, http::StatusCode, web, App, HttpRequest, HttpResponse, Resource, Responder,
};
use actix_web::{dev::Payload, http::StatusCode, web, App, HttpResponse, Responder};
use awc::{Client, ClientResponse};
use futures_core::future::LocalBoxFuture;
use futures_util::TryStreamExt as _;
use super::MultipartForm;
use crate::{
form::{
bytes::Bytes, tempfile::TempFile, text::Text, FieldReader, Limits, MultipartFormConfig,
},
Field, MultipartError,
};
use crate::form::{bytes::Bytes, tempfile::TempFile, text::Text, MultipartFormConfig};
pub async fn send_form(
srv: &TestServer,
@ -769,84 +733,4 @@ mod tests {
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn non_multipart_form_data() {
#[derive(MultipartForm)]
struct TestNonMultipartFormData {
#[allow(unused)]
#[multipart(limit = "30B")]
foo: Text<String>,
}
async fn non_multipart_form_data_route(
_form: MultipartForm<TestNonMultipartFormData>,
) -> String {
unreachable!("request is sent with multipart/mixed");
}
let srv = actix_test::start(|| {
App::new().route("/", web::post().to(non_multipart_form_data_route))
});
let mut form = multipart::Form::default();
form.add_text("foo", "foo");
// mangle content-type, keeping the boundary
let ct = form.content_type().replacen("/form-data", "/mixed", 1);
let res = Client::default()
.post(srv.url("/"))
.content_type(ct)
.send_body(multipart::Body::from(form))
.await
.unwrap();
assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
}
#[should_panic(expected = "called `Result::unwrap()` on an `Err` value: Connect(Disconnected)")]
#[actix_web::test]
async fn field_try_next_panic() {
#[derive(Debug)]
struct NullSink;
impl<'t> FieldReader<'t> for NullSink {
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
fn read_field(
_: &'t HttpRequest,
mut field: Field,
_limits: &'t mut Limits,
) -> Self::Future {
Box::pin(async move {
// exhaust field stream
while let Some(_chunk) = field.try_next().await? {}
// poll again, crash
let _post = field.try_next().await;
Ok(Self)
})
}
}
#[allow(dead_code)]
#[derive(MultipartForm)]
struct NullSinkForm {
foo: NullSink,
}
async fn null_sink(_form: MultipartForm<NullSinkForm>) -> impl Responder {
"unreachable"
}
let srv = actix_test::start(|| App::new().service(Resource::new("/").post(null_sink)));
let mut form = multipart::Form::default();
form.add_text("foo", "data is not important to this test");
// panics with Err(Connect(Disconnected)) due to form NullSink panic
let _res = send_form(&srv, form, "/").await;
}
}

View File

@ -42,36 +42,38 @@ impl<'t> FieldReader<'t> for TempFile {
fn read_field(req: &'t HttpRequest, mut field: Field, limits: &'t mut Limits) -> Self::Future {
Box::pin(async move {
let config = TempFileConfig::from_req(req);
let field_name = field.name().to_owned();
let mut size = 0;
let file = config.create_tempfile().map_err(|err| {
config.map_error(req, &field.form_field_name, TempFileError::FileIo(err))
})?;
let file = config
.create_tempfile()
.map_err(|err| config.map_error(req, &field_name, TempFileError::FileIo(err)))?;
let mut file_async = tokio::fs::File::from_std(file.reopen().map_err(|err| {
config.map_error(req, &field.form_field_name, TempFileError::FileIo(err))
})?);
let mut file_async =
tokio::fs::File::from_std(file.reopen().map_err(|err| {
config.map_error(req, &field_name, TempFileError::FileIo(err))
})?);
while let Some(chunk) = field.try_next().await? {
limits.try_consume_limits(chunk.len(), false)?;
size += chunk.len();
file_async.write_all(chunk.as_ref()).await.map_err(|err| {
config.map_error(req, &field.form_field_name, TempFileError::FileIo(err))
config.map_error(req, &field_name, TempFileError::FileIo(err))
})?;
}
file_async.flush().await.map_err(|err| {
config.map_error(req, &field.form_field_name, TempFileError::FileIo(err))
})?;
file_async
.flush()
.await
.map_err(|err| config.map_error(req, &field_name, TempFileError::FileIo(err)))?;
Ok(TempFile {
file,
content_type: field.content_type().map(ToOwned::to_owned),
file_name: field
.content_disposition()
.expect("multipart form fields should have a content-disposition header")
.get_filename()
.map(ToOwned::to_owned),
.map(str::to_owned),
size,
})
})
@ -82,7 +84,7 @@ impl<'t> FieldReader<'t> for TempFile {
#[non_exhaustive]
pub enum TempFileError {
/// File I/O Error
#[display("File I/O error: {}", _0)]
#[display(fmt = "File I/O error: {}", _0)]
FileIo(std::io::Error),
}
@ -135,7 +137,7 @@ impl TempFileConfig {
};
MultipartError::Field {
name: field_name.to_owned(),
field_name: field_name.to_owned(),
source,
}
}

Some files were not shown because too many files have changed in this diff Show More