1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-08-01 04:51:51 +02:00

Compare commits

...

62 Commits

Author SHA1 Message Date
Rob Ede
0216cb186e add Compress::with_predicate 2023-04-02 05:54:34 +01:00
Rob Ede
2b543437c3 Merge branch 'master' into fix-compression-middleware-images 2023-04-02 04:17:44 +01:00
Rob Ede
97399e8c8c simplify CI 2023-04-02 03:27:14 +01:00
Elijah
8dee8a1426 docs(actix-http-test): update test server example (#3007) 2023-03-31 18:09:13 +00:00
Rob Ede
e68f87f84f add API diff to CI (#3002) 2023-03-15 13:32:55 +00:00
Rob Ede
0f3068f488 ci(windows): use choco to install openssl (#3003
ci: remove openssl install on windows
2023-03-15 05:39:02 +00:00
William R. Arellano
a3ec0ccf99 misc: improve default compress function 2023-03-14 22:21:18 -05:00
William R. Arellano
e9a1aeebce feat(compress): give more control to the user 2023-03-14 22:00:08 -05:00
William R. Arellano
14b09e35d1 feat(compress): use response content type to decide compress 2023-03-14 21:55:15 -05:00
William R. Arellano
2bfc170fb0 feat(compress): add compress function to middleware 2023-03-14 16:40:33 -05:00
William R. Arellano
42fd6290d6 Merge branch 'actix:master' into fix-compression-middleware-images 2023-03-14 16:06:27 -05:00
Rob Ede
5e29726c4f standardize error messages in actix-http 2023-03-13 17:17:02 +00:00
Rob Ede
442fa279da uncomment error variant 2023-03-13 14:30:21 +00:00
Rob Ede
bfdc29ebb8 normalize actix-files error messages 2023-03-13 14:22:50 +00:00
Rob Ede
0e7380659f implement Error for BodyLimitExceeded 2023-03-13 13:40:09 +00:00
Rob Ede
44c5cdaa10 bound initial allocation in to_bytes_limited 2023-03-13 13:40:07 +00:00
Rob Ede
9e7a6fe57b add body::to_bytes_limited (#3000
* add body::to_body_limit

* rename to_bytes_limited
2023-03-13 13:31:48 +00:00
Rob Ede
dfaca18584 add compression_responses benchmark (#3001) 2023-03-12 15:32:07 +00:00
Rob Ede
19c9d858f2 support 16 extractors 2023-03-12 04:29:22 +00:00
Rob Ede
4131786127 remove old benchmarks 2023-03-11 23:20:02 +00:00
Rob Ede
0ba147ef71 update actions/checkout to v3 2023-03-11 23:19:03 +00:00
Rob Ede
3fc01c4887 refactor server binding 2023-03-11 22:17:52 +00:00
Rob Ede
4c4024c949 fix minimal version specs for mime 2023-03-11 22:14:58 +00:00
William R. Arellano
c96844ab14 misc: add unit test for expected behaviour jpeg 2023-03-07 21:11:41 -05:00
William R. Arellano
68adcf657a Add test to check content type image/* 2023-03-07 20:32:37 -05:00
William R. Arellano
99bccb74ac misc: add temporary nix file 2023-03-07 20:32:20 -05:00
Rob Ede
e0939a01fc prepare actix-http release 3.3.1 2023-03-02 17:09:26 +00:00
Rob Ede
20c7c07dc0 fix http version req 2023-03-02 16:21:13 +00:00
Rob Ede
d7c6774ad5 add resource method helpers (#2978) 2023-03-02 08:22:22 +00:00
Rob Ede
67efa4a4db migrate to doc_auto_cfg 2023-02-26 21:55:25 +00:00
Rob Ede
d77bcb0b7c update date in unreleased changelog sections 2023-02-26 21:45:36 +00:00
Rob Ede
c4db9a1ae2 prepare actix-multipart release 0.6.0 2023-02-26 21:44:57 +00:00
Rob Ede
740d0c0c9d prepare actix-multipart-derive release 0.6.0 2023-02-26 21:44:14 +00:00
Rob Ede
f27584046c add todo for header names in next breaking release 2023-02-26 16:31:40 +00:00
Rob Ede
129b78f9c7 prepare actix-test release 0.1.1 2023-02-26 14:20:48 +00:00
Rob Ede
ad27150c5f fix doc tests 2023-02-26 14:14:04 +00:00
Rob Ede
8d5d6a2598 tweak err handlers docs 2023-02-26 13:28:19 +00:00
Rob Ede
e97329eb2a bump socket2 dep to 0.5 2023-02-26 13:28:19 +00:00
Kristian Gaylord
fbfff3e751 actix-test: allow dynamic port setting (#2960)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2023-02-26 05:25:36 +00:00
Rob Ede
fdfb3d45db remove direct dep on ahash for client pool 2023-02-26 03:50:36 +00:00
Rob Ede
4e05629368 specify safe tokio version range 2023-02-26 03:47:25 +00:00
Rob Ede
e35ec28cd2 prepare actix-web release 4.3.1 2023-02-26 03:44:34 +00:00
Rob Ede
35006e9cae prepare actix-web-codegen release 4.2.0 2023-02-26 03:42:27 +00:00
Rob Ede
115701eb86 prepare awc release 3.1.1 2023-02-26 03:34:47 +00:00
Rob Ede
e2fed91efd format markdown with prettier 2023-02-26 03:26:51 +00:00
Jacob Halsey
d4b833ccf0 actix-multipart: Feature: Add typed multipart form extractor (#2883)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2023-02-26 03:26:06 +00:00
Rob Ede
358c1cf85b improve docs for app_config methods 2023-02-22 23:06:23 +00:00
Lioness100
42193bee29 fix typos (#2982) 2023-02-20 08:11:16 +00:00
Rob Ede
dc08ea044b clippy 2023-02-13 21:09:28 +00:00
Roy Wellington Ⅳ
85d88ffada Fix minor typo in Markdown (#2977) 2023-02-12 02:47:42 +00:00
yinho999
bf19a0e761 added body manipulation example for error handlers (#2973)
Closes https://github.com/actix/actix-web/issues/2856
2023-02-09 20:37:01 +00:00
Joel Wurtz
bf1f169be2 [awc] change client::Connect to be public (#2690) 2023-02-09 09:32:04 +00:00
Rob Ede
359d5d5c80 refactor codegen route guards 2023-02-06 17:06:47 +00:00
edgerunnergit
65c0545a7a added support for creating custom methods with route macro (#2969)
Co-authored-by: Rob Ede <robjtede@icloud.com>
Closes https://github.com/actix/actix-web/issues/2893
2023-02-06 12:40:41 +00:00
Rob Ede
b933ed4456 add tests for files_listing_renderer 2023-02-03 21:04:07 -05:00
Rob Ede
4bff1d0abe require safe tokio version range
see https://rustsec.org/advisories/RUSTSEC-2023-0005
2023-02-03 20:35:19 -05:00
Rob Ede
fa106da555 refactor: move Host guard into own module 2023-01-30 11:36:12 -05:00
Rob Ede
c15016dafb prepare actix-files release 0.6.3 2023-01-21 19:03:19 +00:00
Rob Ede
74688843ba prepare actix-http-test release 3.1.0 2023-01-21 19:01:14 +00:00
Rob Ede
845156da85 prepare actix-web-actors release 4.2.0 2023-01-21 19:01:08 +00:00
Rob Ede
98752c053c prepare actix-multipart release 0.5.0 2023-01-21 18:59:13 +00:00
Rob Ede
df6fde883c prepare actix-web release 4.3.0 2023-01-21 18:57:42 +00:00
128 changed files with 4269 additions and 1858 deletions

View File

@@ -3,34 +3,40 @@ name: Bug Report
about: Create a bug report.
---
Your issue may already be reported!
Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one.
Your issue may already be reported! Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one.
## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
- Rust Version (I.e, output of `rustc -V`):

View File

@@ -2,12 +2,14 @@
<!-- Please fill out the following to get your PR reviewed quicker. -->
## PR Type
<!-- What kind of change does this PR make? -->
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
PR_TYPE
## PR Checklist
<!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. -->
@@ -17,11 +19,10 @@ PR_TYPE
- [ ] Format code with the latest stable rustfmt.
- [ ] (Team) Label with affected crates and semver status.
## Overview
<!-- Describe the current and new behavior. -->
<!-- Emphasize any breaking changes. -->
<!-- If this PR fixes or closes an issue, reference it here. -->
<!-- Closes #000 -->

View File

@@ -6,14 +6,18 @@ on:
- master
permissions:
contents: read # to fetch code (actions/checkout)
contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check_benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1

View File

@@ -5,7 +5,11 @@ on:
branches: [master]
permissions:
contents: read # to fetch code (actions/checkout)
contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_and_test_nightly:
@@ -29,7 +33,7 @@ jobs:
CARGO_UNSTABLE_SPARSE_REGISTRY: true
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
# install OpenSSL on Windows
# TODO: GitHub actions docs state that OpenSSL is
@@ -44,7 +48,7 @@ jobs:
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
toolchain: ${{ matrix.version }}
profile: minimal
override: true
@@ -81,7 +85,7 @@ jobs:
- name: Clear the cargo caches
run: |
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
cargo install cargo-cache --version 0.8.3 --no-default-features --features ci-autoclean
cargo-cache
ci_feature_powerset_check:
@@ -93,7 +97,7 @@ jobs:
CARGO_INCREMENTAL: 0
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
@@ -120,7 +124,7 @@ jobs:
CARGO_INCREMENTAL: 0
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable

View File

@@ -7,7 +7,11 @@ on:
branches: [master]
permissions:
contents: read # to fetch code (actions/checkout)
contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_and_test:
@@ -17,7 +21,7 @@ jobs:
target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
version:
- 1.59.0 # MSRV
- stable
@@ -25,30 +29,22 @@ jobs:
name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }}
env:
CI: 1
CARGO_INCREMENTAL: 0
VCPKGRS_DYNAMIC: 1
env: {}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
# install OpenSSL on Windows
# TODO: GitHub actions docs state that OpenSSL is
# already installed on these Windows machines somewhere
- name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install OpenSSL
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: vcpkg install openssl:x64-windows
if: matrix.target.os == 'windows-latest'
run: choco install openssl
- name: Set OpenSSL dir in env
if: matrix.target.os == 'windows-latest'
run: echo 'OPENSSL_DIR=C:\Program Files\OpenSSL-Win64' | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
- name: Install Rust (${{ matrix.version }})
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
toolchain: ${{ matrix.version }}
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
@@ -60,12 +56,6 @@ jobs:
cargo add const-str@0.3 --dev -p=actix-web
cargo add const-str@0.3 --dev -p=awc
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: workaround MSRV issues
if: matrix.version != 'stable'
run: |
@@ -95,42 +85,34 @@ jobs:
- name: Clear the cargo caches
run: |
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
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@v2
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- name: Generate Cargo.lock
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with: { toolchain: nightly }
- name: tests (io-uring)
timeout-minutes: 60
run: >
sudo bash -c "ulimit -Sl 512
&& ulimit -Hl 512
&& PATH=$PATH:/usr/share/rust/.cargo/bin
&& RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && PATH=$PATH:/usr/share/rust/.cargo/bin && RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
rustdoc:
name: doc tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@nightly
- name: Generate Cargo.lock
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1
with: { toolchain: nightly }
- name: doc tests
run: cargo ci-doctest

View File

@@ -4,46 +4,72 @@ 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@v2
- uses: dtolnay/rust-toolchain@nightly
with: { components: rustfmt }
- 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@v2
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: { components: clippy }
- name: Generate Cargo.lock
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Check with Clippy
uses: actions-rs/clippy-check@v1
- uses: giraffate/clippy-action@v1
with:
args: --workspace --tests --examples --all-features
token: ${{ secrets.GITHUB_TOKEN }}
reporter: 'github-pr-check'
github_token: ${{ secrets.GITHUB_TOKEN }}
clippy_flags: --workspace --all-features --tests --examples --bins -- -Dclippy::todo
lint-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: { components: rust-docs }
- name: Check for broken intra-doc links
uses: actions-rs/cargo@v1
env:
RUSTDOCFLAGS: "-D warnings"
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:
command: doc
args: --no-deps --all-features --workspace
ref: ${{ github.base_ref }}
- uses: actions/checkout@v3
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: { toolchain: nightly }
- 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

@@ -6,26 +6,23 @@ on:
push:
branches: [master]
permissions:
contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# job currently (1st Feb 2022) segfaults
coverage:
name: coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: { toolchain: nightly }
- name: Generate coverage file
run: |

View File

@@ -4,16 +4,22 @@ on:
push:
branches: [master]
permissions: {}
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)
contents: write # to push changes in repo (jamesives/github-pages-deploy-action)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@nightly

View File

@@ -1,3 +0,0 @@
{
"proseWrap": "never"
}

1
.prettierrc.yaml Normal file
View File

@@ -0,0 +1 @@
proseWrap: never

View File

@@ -5,6 +5,7 @@ members = [
"actix-http-test",
"actix-http",
"actix-multipart",
"actix-multipart-derive",
"actix-router",
"actix-test",
"actix-web-actors",
@@ -27,6 +28,7 @@ actix-files = { path = "actix-files" }
actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" }
actix-multipart = { path = "actix-multipart" }
actix-multipart-derive = { path = "actix-multipart-derive" }
actix-router = { path = "actix-router" }
actix-test = { path = "actix-test" }
actix-web = { path = "actix-web" }

View File

@@ -1,6 +1,9 @@
# Changes
## Unreleased - 2022-xx-xx
## Unreleased - 2023-xx-xx
## 0.6.3 - 2023-01-21
- XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
- Update `tokio-uring` dependency to `0.4`.
@@ -8,13 +11,14 @@
[#2903]: https://github.com/actix/actix-web/pull/2903
## 0.6.2 - 2022-07-23
- Allow partial range responses for video content to start streaming sooner. [#2817]
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#2817]: https://github.com/actix/actix-web/pull/2817
## 0.6.1 - 2022-06-11
- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021]
- Update `tokio-uring` dependency to `0.3`.
- Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645]
@@ -23,46 +27,46 @@
[#2021]: https://github.com/actix/actix-web/pull/2021
[#2645]: https://github.com/actix/actix-web/pull/2645
## 0.6.0 - 2022-02-25
- No significant changes since `0.6.0-beta.16`.
## 0.6.0-beta.16 - 2022-01-31
- No significant changes since `0.6.0-beta.15`.
## 0.6.0-beta.15 - 2022-01-21
- No significant changes since `0.6.0-beta.14`.
## 0.6.0-beta.14 - 2022-01-14
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
[#2583]: https://github.com/actix/actix-web/pull/2583
## 0.6.0-beta.13 - 2022-01-04
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
- Minimum supported Rust version (MSRV) is now 1.54.
[#2398]: https://github.com/actix/actix-web/pull/2398
## 0.6.0-beta.12 - 2021-12-29
- No significant changes since `0.6.0-beta.11`.
## 0.6.0-beta.11 - 2021-12-27
- No significant changes since `0.6.0-beta.10`.
## 0.6.0-beta.10 - 2021-12-11
- No significant changes since `0.6.0-beta.9`.
## 0.6.0-beta.9 - 2021-11-22
- Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
- Add `NamedFile::open_async`. [#2408]
- Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
@@ -73,24 +77,24 @@
[#2408]: https://github.com/actix/actix-web/pull/2408
[#2453]: https://github.com/actix/actix-web/pull/2453
## 0.6.0-beta.8 - 2021-10-20
- Minimum supported Rust version (MSRV) is now 1.52.
## 0.6.0-beta.7 - 2021-09-09
- Minimum supported Rust version (MSRV) is now 1.51.
## 0.6.0-beta.6 - 2021-06-26
- Added `Files::path_filter()`. [#2274]
- `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
[#2274]: https://github.com/actix/actix-web/pull/2274
[#2228]: https://github.com/actix/actix-web/pull/2228
## 0.6.0-beta.5 - 2021-06-17
- `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
- For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
- `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
@@ -101,58 +105,58 @@
[#2225]: https://github.com/actix/actix-web/pull/2225
[#2257]: https://github.com/actix/actix-web/pull/2257
## 0.6.0-beta.4 - 2021-04-02
- Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
[#2046]: https://github.com/actix/actix-web/pull/2046
## 0.6.0-beta.3 - 2021-03-09
- No notable changes.
## 0.6.0-beta.2 - 2021-02-10
- Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
- Replace `v_htmlescape` with `askama_escape`. [#1953]
[#1887]: https://github.com/actix/actix-web/pull/1887
[#1953]: https://github.com/actix/actix-web/pull/1953
## 0.6.0-beta.1 - 2021-01-07
- `HttpRange::parse` now has its own error type.
- Update `bytes` to `1.0`. [#1813]
[#1813]: https://github.com/actix/actix-web/pull/1813
## 0.5.0 - 2020-12-26
- Optionally support hidden files/directories. [#1811]
[#1811]: https://github.com/actix/actix-web/pull/1811
## 0.4.1 - 2020-11-24
- Clarify order of parameters in `Files::new` and improve docs.
## 0.4.0 - 2020-10-06
- Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
[#1714]: https://github.com/actix/actix-web/pull/1714
## 0.3.0 - 2020-09-11
- No significant changes from 0.3.0-beta.1.
## 0.3.0-beta.1 - 2020-07-15
- Update `v_htmlescape` to 0.10
- Update `actix-web` and `actix-http` dependencies to beta.1
## 0.3.0-alpha.1 - 2020-05-23
- Update `actix-web` and `actix-http` dependencies to alpha
- Fix some typos in the docs
- Bump minimum supported Rust version to 1.40
@@ -160,73 +164,73 @@
[#1384]: https://github.com/actix/actix-web/pull/1384
## 0.2.1 - 2019-12-22
- Use the same format for file URLs regardless of platforms
## 0.2.0 - 2019-12-20
- Fix BodyEncoding trait import #1220
## 0.2.0-alpha.1 - 2019-12-07
- Migrate to `std::future`
## 0.1.7 - 2019-11-06
- Add an additional `filename*` param in the `Content-Disposition` header of
`actix_files::NamedFile` to be more compatible. (#1151)
- Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
## 0.1.6 - 2019-10-14
- Add option to redirect to a slash-ended path `Files` #1132
## 0.1.5 - 2019-10-08
- Bump up `mime_guess` crate version to 2.0.1
- Bump up `percent-encoding` crate version to 2.1
- Allow user defined request guards for `Files` #1113
## 0.1.4 - 2019-07-20
- Allow to disable `Content-Disposition` header #686
## 0.1.3 - 2019-06-28
- Do not set `Content-Length` header, let actix-http set it #930
## 0.1.2 - 2019-06-13
- Content-Length is 0 for NamedFile HEAD request #914
- Fix ring dependency from actix-web default features for #741
## 0.1.1 - 2019-06-01
- Static files are incorrectly served as both chunked and with length #812
## 0.1.0 - 2019-05-25
- NamedFile last-modified check always fails due to nano-seconds in file modified date #820
## 0.1.0-beta.4 - 2019-05-12
- Update actix-web to beta.4
## 0.1.0-beta.1 - 2019-04-20
- Update actix-web to beta.1
## 0.1.0-alpha.6 - 2019-04-14
- Update actix-web to alpha6
## 0.1.0-alpha.4 - 2019-04-08
- Update actix-web to alpha4
## 0.1.0-alpha.2 - 2019-04-02
- Add default handler support
## 0.1.0-alpha.1 - 2019-03-28
- Initial impl

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.6.2"
version = "0.6.3"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
@@ -32,11 +32,11 @@ derive_more = "0.99.5"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http-range = "0.1.4"
log = "0.4"
mime = "0.3"
mime = "0.3.9"
mime_guess = "2.0.1"
percent-encoding = "2.1"
pin-project-lite = "0.2.7"
v_htmlescape= "0.15"
v_htmlescape = "0.15.5"
# experimental-io-uring
[target.'cfg(target_os = "linux")'.dependencies]

View File

@@ -3,11 +3,11 @@
> 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.2)](https://docs.rs/actix-files/0.6.2)
[![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.59+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.2/status.svg)](https://deps.rs/crate/actix-files/0.6.2)
[![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)
@@ -15,4 +15,4 @@
- [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.54
- Minimum Supported Rust Version (MSRV): 1.59

View File

@@ -4,46 +4,45 @@ use derive_more::Display;
/// Errors which can occur when serving static files.
#[derive(Debug, PartialEq, Eq, Display)]
pub enum FilesError {
/// Path is not a directory
/// Path is not a directory.
#[allow(dead_code)]
#[display(fmt = "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(fmt = "Unable to render directory without index file")]
/// Cannot render directory.
#[display(fmt = "unable to render directory without index file")]
IsDirectory,
}
/// Return `NotFound` for `FilesError`
impl ResponseError for FilesError {
/// Returns `404 Not Found`.
fn status_code(&self) -> StatusCode {
StatusCode::NOT_FOUND
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, PartialEq, Eq, Display)]
#[non_exhaustive]
pub enum UriSegmentError {
/// The segment started with the wrapped invalid character.
#[display(fmt = "The segment started with the wrapped invalid character")]
/// Segment started with the wrapped invalid character.
#[display(fmt = "segment started with invalid character: ('{_0}')")]
BadStart(char),
/// The segment contained the wrapped invalid character.
#[display(fmt = "The segment contained the wrapped invalid character")]
/// Segment contained the wrapped invalid character.
#[display(fmt = "segment contained invalid character ('{_0}')")]
BadChar(char),
/// The segment ended with the wrapped invalid character.
#[display(fmt = "The segment ended with the wrapped invalid character")]
/// Segment ended with the wrapped invalid character.
#[display(fmt = "segment ended with invalid character: ('{_0}')")]
BadEnd(char),
/// The path is not a valid UTF-8 string after doing percent decoding.
#[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")]
/// 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,
}
/// Return `BadRequest` for `UriSegmentError`
impl ResponseError for UriSegmentError {
/// Returns `400 Bad Request`.
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}

View File

@@ -142,7 +142,7 @@ impl Files {
self
}
/// Set custom directory renderer
/// Set custom directory renderer.
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where
for<'r, 's> F:
@@ -152,7 +152,7 @@ impl Files {
self
}
/// Specifies mime override callback
/// Specifies MIME override callback.
pub fn mime_override<F>(mut self, f: F) -> Self
where
F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
@@ -390,3 +390,42 @@ impl ServiceFactory<ServiceRequest> for Files {
}
}
}
#[cfg(test)]
mod tests {
use actix_web::{
http::StatusCode,
test::{self, TestRequest},
App, HttpResponse,
};
use super::*;
#[actix_web::test]
async fn custom_files_listing_renderer() {
let srv = test::init_service(
App::new().service(
Files::new("/", "./tests")
.show_files_listing()
.files_listing_renderer(|dir, req| {
Ok(ServiceResponse::new(
req.clone(),
HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()),
))
}),
),
)
.await;
let req = TestRequest::with_uri("/").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let body = test::read_body(res).await;
assert!(
body.ends_with(b"actix-files/tests/"),
"body {:?} does not end with `actix-files/tests/`",
body
);
}
}

View File

@@ -14,6 +14,9 @@
#![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))]
use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{

View File

@@ -30,7 +30,7 @@ impl PathBufWrap {
let mut segment_count = path.matches('/').count() + 1;
// we can decode the whole path here (instead of per-segment decoding)
// because we will reject `%2F` in paths using `segement_count`.
// because we will reject `%2F` in paths using `segment_count`.
let path = percent_encoding::percent_decode_str(path)
.decode_utf8()
.map_err(|_| UriSegmentError::NotValidUtf8)?;

View File

@@ -1,4 +1,36 @@
use derive_more::{Display, Error};
use std::fmt;
use derive_more::Error;
/// Copy of `http_range::HttpRangeParseError`.
#[derive(Debug, Clone)]
enum HttpRangeParseError {
InvalidRange,
NoOverlap,
}
impl From<http_range::HttpRangeParseError> for HttpRangeParseError {
fn from(err: http_range::HttpRangeParseError) -> Self {
match err {
http_range::HttpRangeParseError::InvalidRange => Self::InvalidRange,
http_range::HttpRangeParseError::NoOverlap => Self::NoOverlap,
}
}
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub struct ParseRangeErr(#[error(not(source))] HttpRangeParseError);
impl fmt::Display for ParseRangeErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid Range header: ")?;
f.write_str(match self.0 {
HttpRangeParseError::InvalidRange => "invalid syntax",
HttpRangeParseError::NoOverlap => "range starts after end of content",
})
}
}
/// HTTP Range header representation.
#[derive(Debug, Clone, Copy)]
@@ -10,26 +42,22 @@ pub struct HttpRange {
pub length: u64,
}
#[derive(Debug, Clone, Display, Error)]
#[display(fmt = "Parse HTTP Range failed")]
pub struct ParseRangeErr(#[error(not(source))] ());
impl HttpRange {
/// Parses Range HTTP header string as per RFC 2616.
///
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
/// `size` is full size of response (file).
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
match http_range::HttpRange::parse(header, size) {
Ok(ranges) => Ok(ranges
.iter()
.map(|range| HttpRange {
start: range.start,
length: range.length,
})
.collect()),
Err(_) => Err(ParseRangeErr(())),
}
let ranges = http_range::HttpRange::parse(header, size)
.map_err(|err| ParseRangeErr(err.into()))?;
Ok(ranges
.iter()
.map(|range| HttpRange {
start: range.start,
length: range.length,
})
.collect())
}
}

View File

@@ -1,10 +1,13 @@
# Changes
## Unreleased - 2022-xx-xx
## Unreleased - 2023-xx-xx
## 3.1.0 - 2023-01-21
- Minimum supported Rust version (MSRV) is now 1.59.
## 3.0.0 - 2022-07-24
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
- Added `TestServer::client_headers` method. [#2097]
- Update `actix-server` dependency to `2`.
@@ -16,71 +19,71 @@
[#2097]: https://github.com/actix/actix-web/pull/2097
[#1813]: https://github.com/actix/actix-web/pull/1813
<details>
<summary>3.0.0 Pre-Releases</summary>
## 3.0.0-beta.13 - 2022-02-16
- No significant changes since `3.0.0-beta.12`.
## 3.0.0-beta.12 - 2022-01-31
- No significant changes since `3.0.0-beta.11`.
## 3.0.0-beta.11 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.
## 3.0.0-beta.10 - 2021-12-27
- Update `actix-server` to `2.0.0-rc.2`. [#2550]
[#2550]: https://github.com/actix/actix-web/pull/2550
## 3.0.0-beta.9 - 2021-12-11
- No significant changes since `3.0.0-beta.8`.
## 3.0.0-beta.8 - 2021-11-30
- Update `actix-tls` to `3.0.0-rc.1`. [#2474]
[#2474]: https://github.com/actix/actix-web/pull/2474
## 3.0.0-beta.7 - 2021-11-22
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
[#2408]: https://github.com/actix/actix-web/pull/2408
## 3.0.0-beta.6 - 2021-11-15
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
- Update `actix-server` to `2.0.0-beta.9`. [#2442]
- Minimum supported Rust version (MSRV) is now 1.52.
[#2442]: https://github.com/actix/actix-web/pull/2442
## 3.0.0-beta.5 - 2021-09-09
- Minimum supported Rust version (MSRV) is now 1.51.
## 3.0.0-beta.4 - 2021-04-02
- Added `TestServer::client_headers` method. [#2097]
[#2097]: https://github.com/actix/actix-web/pull/2097
## 3.0.0-beta.3 - 2021-03-09
- No notable changes.
- No notable changes.
## 3.0.0-beta.2 - 2021-02-10
- No notable changes.
## 3.0.0-beta.1 - 2021-01-07
- Update `bytes` to `1.0`. [#1813]
[#1813]: https://github.com/actix/actix-web/pull/1813
@@ -88,6 +91,7 @@
</details>
## 2.1.0 - 2020-11-25
- Add ability to set address for `TestServer`. [#1645]
- Upgrade `base64` to `0.13`.
- Upgrade `serde_urlencoded` to `0.7`. [#1773]
@@ -95,12 +99,12 @@
[#1773]: https://github.com/actix/actix-web/pull/1773
[#1645]: https://github.com/actix/actix-web/pull/1645
## 2.0.0 - 2020-09-11
- Update actix-codec and actix-utils dependencies.
## 2.0.0-alpha.1 - 2020-05-23
- Update the `time` dependency to 0.2.7
- Update `actix-connect` dependency to 2.0.0-alpha.2
- Make `test_server` `async` fn.
@@ -110,55 +114,56 @@
- Update `env_logger` dependency to 0.7
## 1.0.0 - 2019-12-13
- Replaced `TestServer::start()` with `test_server()`
## 1.0.0-alpha.3 - 2019-12-07
- Migrate to `std::future`
## 0.2.5 - 2019-09-17
- Update serde_urlencoded to "0.6.1"
- Increase TestServerRuntime timeouts from 500ms to 3000ms
- Do not override current `System`
## 0.2.4 - 2019-07-18
- Update actix-server to 0.6
## 0.2.3 - 2019-07-16
- Add `delete`, `options`, `patch` methods to `TestServerRunner`
## 0.2.2 - 2019-06-16
- Add .put() and .sput() methods
## 0.2.1 - 2019-06-05
- Add license files
## 0.2.0 - 2019-05-12
- Update awc and actix-http deps
## 0.1.1 - 2019-04-24
- Always make new connection for http client
## 0.1.0 - 2019-04-16
- No changes
## 0.1.0-alpha.3 - 2019-04-02
- Request functions accept path #743
## 0.1.0-alpha.2 - 2019-03-29
- Added TestServerRuntime::load_body() method
- Update actix-http and awc libraries
## 0.1.0-alpha.1 - 2019-03-28
- Initial impl

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-http-test"
version = "3.0.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"]
@@ -39,15 +39,15 @@ awc = { version = "3", default-features = false }
bytes = "1"
futures-core = { version = "0.3.17", default-features = false }
http = "0.2.5"
http = "0.2.7"
log = "0.4"
socket2 = "0.4"
serde = "1.0"
serde_json = "1.0"
serde = "1"
serde_json = "1"
slab = "0.4"
serde_urlencoded = "0.7"
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.18.4", features = ["sync"] }
tokio = { version = "1.24.2", features = ["sync"] }
[dev-dependencies]
actix-web = { version = "4", default-features = false, features = ["cookies"] }

View File

@@ -3,15 +3,15 @@
> 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.0.0)](https://docs.rs/actix-http-test/3.0.0)
[![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.59+-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.0.0/status.svg)](https://deps.rs/crate/actix-http-test/3.0.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)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http-test)
- Minimum Supported Rust Version (MSRV): 1.54
- Minimum Supported Rust Version (MSRV): 1.59

View File

@@ -5,6 +5,7 @@
#![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))]
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;
@@ -33,7 +34,9 @@ use tokio::sync::mpsc;
/// ```no_run
/// use actix_http::HttpService;
/// use actix_http_test::test_server;
/// use actix_web::{web, App, HttpResponse, Error};
/// use actix_service::map_config;
/// use actix_service::ServiceFactoryExt;
/// use actix_web::{dev::AppConfig, web, App, Error, HttpResponse};
///
/// async fn my_handler() -> Result<HttpResponse, Error> {
/// Ok(HttpResponse::Ok().into())
@@ -41,14 +44,19 @@ use tokio::sync::mpsc;
///
/// #[actix_web::test]
/// async fn test_example() {
/// let mut srv = TestServer::start(||
/// HttpService::new(
/// App::new().service(web::resource("/").to(my_handler))
/// )
/// );
/// let srv = test_server(|| {
/// let app = App::new().service(web::resource("/").to(my_handler));
///
/// HttpService::build()
/// .h1(map_config(app, |_| AppConfig::default()))
/// .tcp()
/// .map_err(|_| ())
/// })
/// .await;
///
/// let req = srv.get("/");
/// let response = req.send().await.unwrap();
///
/// assert!(response.status().is_success());
/// }
/// ```

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "3.3.0"
version = "3.3.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
@@ -61,23 +61,23 @@ actix-codec = "0.5"
actix-utils = "3"
actix-rt = { version = "2.2", default-features = false }
ahash = "0.7"
ahash = "0.8"
bitflags = "1.2"
bytes = "1"
bytestring = "1"
derive_more = "0.99.5"
encoding_rs = "0.8"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http = "0.2.5"
http = "0.2.7"
httparse = "1.5.1"
httpdate = "1.0.1"
itoa = "1"
language-tags = "0.3"
mime = "0.3"
mime = "0.3.4"
percent-encoding = "2.1"
pin-project-lite = "0.2"
smallvec = "1.6.1"
tokio = { version = "1.18.4", features = [] }
tokio = { version = "1.24.2", features = [] }
tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
@@ -119,24 +119,13 @@ serde_json = "1.0"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.18.4", features = ["net", "rt", "macros"] }
tokio = { version = "1.24.2", features = ["net", "rt", "macros"] }
[[example]]
name = "ws"
required-features = ["ws", "rustls"]
[[bench]]
name = "write-camel-case"
harness = false
[[bench]]
name = "status-line"
harness = false
[[bench]]
name = "uninit-headers"
harness = false
[[bench]]
name = "quality-value"
name = "response-body-compression"
harness = false
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]

View File

@@ -3,18 +3,18 @@
> 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.3.0)](https://docs.rs/actix-http/3.3.0)
[![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.59+-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.3.0/status.svg)](https://deps.rs/crate/actix-http/3.3.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)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http)
- Minimum Supported Rust Version (MSRV): 1.54
- Minimum Supported Rust Version (MSRV): 1.59
## Example
@@ -49,18 +49,3 @@ async fn main() -> io::Result<()> {
.await
}
```
## License
This project is licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
at your option.
## Code of Conduct
Contribution to the actix-http crate is organized under the terms of the
Contributor Covenant, the maintainer of actix-http, @fafhrd91, promises to
intervene to uphold that code of conduct.

View File

@@ -1,97 +0,0 @@
#![allow(clippy::uninlined_format_args)]
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
fn bench_quality_display_impls(c: &mut Criterion) {
let mut group = c.benchmark_group("quality value display impls");
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
b.iter(|| _new::Quality(i).to_string())
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| _naive::Quality(i).to_string())
});
}
group.finish();
}
criterion_group!(benches, bench_quality_display_impls);
criterion_main!(benches);
mod _new {
use std::fmt;
pub struct Quality(pub(crate) u16);
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
1000 => f.write_str("1"),
// some number in the range 1999
x => {
f.write_str("0.")?;
// this implementation avoids string allocation otherwise required
// for `.trim_end_matches('0')`
if x < 10 {
f.write_str("00")?;
// 0 is handled so it's not possible to have a trailing 0, we can just return
itoa_fmt(f, x)
} else if x < 100 {
f.write_str("0")?;
if x % 10 == 0 {
// trailing 0, divide by 10 and write
itoa_fmt(f, x / 10)
} else {
itoa_fmt(f, x)
}
} else {
// x is in range 101999
if x % 100 == 0 {
// two trailing 0s, divide by 100 and write
itoa_fmt(f, x / 100)
} else if x % 10 == 0 {
// one trailing 0, divide by 10 and write
itoa_fmt(f, x / 10)
} else {
itoa_fmt(f, x)
}
}
}
}
}
}
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
let mut buf = itoa::Buffer::new();
wr.write_str(buf.format(value))
}
}
mod _naive {
use std::fmt;
pub struct Quality(pub(crate) u16);
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
1000 => f.write_str("1"),
x => {
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
}
}
}
}
}

View File

@@ -0,0 +1,90 @@
#![allow(clippy::uninlined_format_args)]
use std::convert::Infallible;
use actix_http::{encoding::Encoder, ContentEncoding, Request, Response, StatusCode};
use actix_service::{fn_service, Service as _};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
static BODY: &[u8] = include_bytes!("../Cargo.toml");
fn compression_responses(c: &mut Criterion) {
let mut group = c.benchmark_group("compression responses");
group.bench_function("identity", |b| {
let rt = actix_rt::Runtime::new().unwrap();
let identity_svc = fn_service(|_: Request| async move {
let mut res = Response::with_body(StatusCode::OK, ());
let body = black_box(Encoder::response(
ContentEncoding::Identity,
res.head_mut(),
BODY,
));
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
});
b.iter(|| {
rt.block_on(identity_svc.call(Request::new())).unwrap();
});
});
group.bench_function("gzip", |b| {
let rt = actix_rt::Runtime::new().unwrap();
let identity_svc = fn_service(|_: Request| async move {
let mut res = Response::with_body(StatusCode::OK, ());
let body = black_box(Encoder::response(
ContentEncoding::Gzip,
res.head_mut(),
BODY,
));
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
});
b.iter(|| {
rt.block_on(identity_svc.call(Request::new())).unwrap();
});
});
group.bench_function("br", |b| {
let rt = actix_rt::Runtime::new().unwrap();
let identity_svc = fn_service(|_: Request| async move {
let mut res = Response::with_body(StatusCode::OK, ());
let body = black_box(Encoder::response(
ContentEncoding::Brotli,
res.head_mut(),
BODY,
));
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
});
b.iter(|| {
rt.block_on(identity_svc.call(Request::new())).unwrap();
});
});
group.bench_function("zstd", |b| {
let rt = actix_rt::Runtime::new().unwrap();
let identity_svc = fn_service(|_: Request| async move {
let mut res = Response::with_body(StatusCode::OK, ());
let body = black_box(Encoder::response(
ContentEncoding::Zstd,
res.head_mut(),
BODY,
));
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
});
b.iter(|| {
rt.block_on(identity_svc.call(Request::new())).unwrap();
});
});
group.finish();
}
criterion_group!(benches, compression_responses);
criterion_main!(benches);

View File

@@ -1,214 +0,0 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use bytes::BytesMut;
use http::Version;
const CODES: &[u16] = &[201, 303, 404, 515];
fn bench_write_status_line_11(c: &mut Criterion) {
let mut group = c.benchmark_group("write_status_line v1.1");
let version = Version::HTTP_11;
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_original::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_new::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_naive::write_status_line(version, i, &mut b);
})
});
}
group.finish();
}
fn bench_write_status_line_10(c: &mut Criterion) {
let mut group = c.benchmark_group("write_status_line v1.0");
let version = Version::HTTP_10;
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_original::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_new::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_naive::write_status_line(version, i, &mut b);
})
});
}
group.finish();
}
fn bench_write_status_line_09(c: &mut Criterion) {
let mut group = c.benchmark_group("write_status_line v0.9");
let version = Version::HTTP_09;
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_original::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_new::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_naive::write_status_line(version, i, &mut b);
})
});
}
group.finish();
}
criterion_group!(
benches,
bench_write_status_line_11,
bench_write_status_line_10,
bench_write_status_line_09
);
criterion_main!(benches);
mod _naive {
use bytes::{BufMut, BytesMut};
use http::Version;
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
match version {
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
_ => {
// other HTTP version handlers do not use this method
}
}
bytes.put_slice(n.to_string().as_bytes());
}
}
mod _new {
use bytes::{BufMut, BytesMut};
use http::Version;
const DIGITS_START: u8 = b'0';
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
match version {
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
_ => {
// other HTTP version handlers do not use this method
}
}
let d100 = (n / 100) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
bytes.put_u8(b' ');
}
}
mod _original {
use std::ptr;
use bytes::{BufMut, BytesMut};
use http::Version;
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
2021222324252627282930313233343536373839\
4041424344454647484950515253545556575859\
6061626364656667686970717273747576777879\
8081828384858687888990919293949596979899";
pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13;
pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) {
let mut buf: [u8; STATUS_LINE_BUF_SIZE] = *b"HTTP/1.1 ";
match version {
Version::HTTP_2 => buf[5] = b'2',
Version::HTTP_10 => buf[7] = b'0',
Version::HTTP_09 => {
buf[5] = b'0';
buf[7] = b'9';
}
_ => {}
}
let mut curr: isize = 12;
let buf_ptr = buf.as_mut_ptr();
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
let four = n > 999;
// decode 2 more chars, if > 2 chars
let d1 = (n % 100) << 1;
n /= 100;
curr -= 2;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
}
// decode last 1 or 2 chars
if n < 10 {
curr -= 1;
unsafe {
*buf_ptr.offset(curr) = (n as u8) + b'0';
}
} else {
let d1 = n << 1;
curr -= 2;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
}
}
bytes.put_slice(&buf);
if four {
bytes.put_u8(b' ');
}
}
}

View File

@@ -1,135 +0,0 @@
use criterion::{criterion_group, criterion_main, Criterion};
use bytes::BytesMut;
// A Miri run detects UB, seen on this playground:
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915
fn bench_header_parsing(c: &mut Criterion) {
c.bench_function("Original (Unsound) [short]", |b| {
b.iter(|| {
let mut buf = BytesMut::from(REQ_SHORT);
_original::parse_headers(&mut buf);
})
});
c.bench_function("New (safe) [short]", |b| {
b.iter(|| {
let mut buf = BytesMut::from(REQ_SHORT);
_new::parse_headers(&mut buf);
})
});
c.bench_function("Original (Unsound) [realistic]", |b| {
b.iter(|| {
let mut buf = BytesMut::from(REQ);
_original::parse_headers(&mut buf);
})
});
c.bench_function("New (safe) [realistic]", |b| {
b.iter(|| {
let mut buf = BytesMut::from(REQ);
_new::parse_headers(&mut buf);
})
});
}
criterion_group!(benches, bench_header_parsing);
criterion_main!(benches);
const MAX_HEADERS: usize = 96;
const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
[httparse::EMPTY_HEADER; MAX_HEADERS];
#[derive(Clone, Copy)]
struct HeaderIndex {
name: (usize, usize),
value: (usize, usize),
}
const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
name: (0, 0),
value: (0, 0),
};
const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = [EMPTY_HEADER_INDEX; MAX_HEADERS];
impl HeaderIndex {
fn record(bytes: &[u8], headers: &[httparse::Header<'_>], indices: &mut [HeaderIndex]) {
let bytes_ptr = bytes.as_ptr() as usize;
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
let name_start = header.name.as_ptr() as usize - bytes_ptr;
let name_end = name_start + header.name.len();
indices.name = (name_start, name_end);
let value_start = header.value.as_ptr() as usize - bytes_ptr;
let value_end = value_start + header.value.len();
indices.value = (value_start, value_end);
}
}
}
// test cases taken from:
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
const REQ_SHORT: &[u8] = b"\
GET / HTTP/1.0\r\n\
Host: example.com\r\n\
Cookie: session=60; user_id=1\r\n\r\n";
const REQ: &[u8] = b"\
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
Host: www.kittyhell.com\r\n\
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\
Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\
Accept-Encoding: gzip,deflate\r\n\
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\
Keep-Alive: 115\r\n\
Connection: keep-alive\r\n\
Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n";
mod _new {
use super::*;
pub fn parse_headers(src: &mut BytesMut) -> usize {
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
let mut req = httparse::Request::new(&mut parsed);
match req.parse(src).unwrap() {
httparse::Status::Complete(_len) => {
HeaderIndex::record(src, req.headers, &mut headers);
req.headers.len()
}
_ => unreachable!(),
}
}
}
mod _original {
use super::*;
use std::mem::MaybeUninit;
pub fn parse_headers(src: &mut BytesMut) -> usize {
#![allow(invalid_value, clippy::uninit_assumed_init)]
let mut headers: [HeaderIndex; MAX_HEADERS] =
unsafe { MaybeUninit::uninit().assume_init() };
#[allow(invalid_value)]
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
unsafe { MaybeUninit::uninit().assume_init() };
let mut req = httparse::Request::new(&mut parsed);
match req.parse(src).unwrap() {
httparse::Status::Complete(_len) => {
HeaderIndex::record(src, req.headers, &mut headers);
req.headers.len()
}
_ => unreachable!(),
}
}
}

View File

@@ -1,93 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
fn bench_write_camel_case(c: &mut Criterion) {
let mut group = c.benchmark_group("write_camel_case");
let names = ["connection", "Transfer-Encoding", "transfer-encoding"];
for &i in &names {
let bts = i.as_bytes();
group.bench_with_input(BenchmarkId::new("Original", i), bts, |b, bts| {
b.iter(|| {
let mut buf = black_box([0; 24]);
_original::write_camel_case(black_box(bts), &mut buf)
});
});
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
b.iter(|| {
let mut buf = black_box([0; 24]);
let len = black_box(bts.len());
_new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len)
});
});
}
group.finish();
}
criterion_group!(benches, bench_write_camel_case);
criterion_main!(benches);
mod _new {
pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
// first copy entire (potentially wrong) slice to output
let buffer = unsafe {
std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len);
std::slice::from_raw_parts_mut(buf, len)
};
let mut iter = value.iter();
// first character should be uppercase
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[0] = c & 0b1101_1111;
}
// track 1 ahead of the current position since that's the location being assigned to
let mut index = 2;
// remaining characters after hyphens should also be uppercase
while let Some(&c) = iter.next() {
if c == b'-' {
// advance iter by one and uppercase if needed
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[index] = c & 0b1101_1111;
}
}
index += 1;
}
}
}
mod _original {
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
let mut index = 0;
let key = value;
let mut key_iter = key.iter();
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
} else {
return;
}
while let Some(c) = key_iter.next() {
buffer[index] = *c;
index += 1;
if *c == b'-' {
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
}
}
}
}
}

View File

@@ -22,4 +22,4 @@ pub(crate) use self::message_body::MessageBodyMapErr;
pub use self::none::None;
pub use self::size::BodySize;
pub use self::sized_stream::SizedStream;
pub use self::utils::to_bytes;
pub use self::utils::{to_bytes, to_bytes_limited, BodyLimitExceeded};

View File

@@ -3,75 +3,196 @@ use std::task::Poll;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use derive_more::{Display, Error};
use futures_core::ready;
use super::{BodySize, MessageBody};
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
/// Collects all the bytes produced by `body`.
///
/// Any errors produced by the body stream are returned immediately.
///
/// Consider using [`to_bytes_limited`] instead to protect against memory exhaustion.
///
/// # Examples
///
/// ```
/// use actix_http::body::{self, to_bytes};
/// use bytes::Bytes;
///
/// # async fn test_to_bytes() {
/// # actix_rt::System::new().block_on(async {
/// let body = body::None::new();
/// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty());
///
/// let body = Bytes::from_static(b"123");
/// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, b"123"[..]);
/// # }
/// assert_eq!(bytes, "123");
/// # });
/// ```
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
to_bytes_limited(body, usize::MAX)
.await
.expect("body should never yield more than usize::MAX bytes")
}
/// Error type returned from [`to_bytes_limited`] when body produced exceeds limit.
#[derive(Debug, Display, Error)]
#[display(fmt = "limit exceeded while collecting body bytes")]
#[non_exhaustive]
pub struct BodyLimitExceeded;
/// Collects the bytes produced by `body`, up to `limit` bytes.
///
/// If a chunk read from `poll_next` causes the total number of bytes read to exceed `limit`, an
/// `Err(BodyLimitExceeded)` is returned.
///
/// Any errors produced by the body stream are returned immediately as `Ok(Err(B::Error))`.
///
/// # Examples
///
/// ```
/// use actix_http::body::{self, to_bytes_limited};
/// use bytes::Bytes;
///
/// # actix_rt::System::new().block_on(async {
/// let body = body::None::new();
/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap();
/// assert!(bytes.is_empty());
///
/// let body = Bytes::from_static(b"123");
/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap();
/// assert_eq!(bytes, "123");
///
/// let body = Bytes::from_static(b"123");
/// assert!(to_bytes_limited(body, 2).await.is_err());
/// # });
/// ```
pub async fn to_bytes_limited<B: MessageBody>(
body: B,
limit: usize,
) -> Result<Result<Bytes, B::Error>, BodyLimitExceeded> {
/// Sensible default (32kB) for initial, bounded allocation when collecting body bytes.
const INITIAL_ALLOC_BYTES: usize = 32 * 1024;
let cap = match body.size() {
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize,
// good enough first guess for chunk size
BodySize::Stream => 32_768,
BodySize::None | BodySize::Sized(0) => return Ok(Ok(Bytes::new())),
BodySize::Sized(size) if size as usize > limit => return Err(BodyLimitExceeded),
BodySize::Sized(size) => (size as usize).min(INITIAL_ALLOC_BYTES),
BodySize::Stream => INITIAL_ALLOC_BYTES,
};
let mut exceeded_limit = false;
let mut buf = BytesMut::with_capacity(cap);
pin!(body);
poll_fn(|cx| loop {
match poll_fn(|cx| loop {
let body = body.as_mut();
match ready!(body.poll_next(cx)) {
Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
Some(Ok(bytes)) => {
// if limit is exceeded...
if buf.len() + bytes.len() > limit {
// ...set flag to true and break out of poll_fn
exceeded_limit = true;
return Poll::Ready(Ok(()));
}
buf.extend_from_slice(&bytes)
}
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await?;
.await
{
// propagate error returned from body poll
Err(err) => Ok(Err(err)),
Ok(buf.freeze())
// limit was exceeded while reading body
Ok(()) if exceeded_limit => Err(BodyLimitExceeded),
// otherwise return body buffer
Ok(()) => Ok(Ok(buf.freeze())),
}
}
#[cfg(test)]
mod test {
mod tests {
use std::io;
use futures_util::{stream, StreamExt as _};
use super::*;
use crate::{body::BodyStream, Error};
use crate::{
body::{BodyStream, SizedStream},
Error,
};
#[actix_rt::test]
async fn test_to_bytes() {
async fn to_bytes_complete() {
let bytes = to_bytes(()).await.unwrap();
assert!(bytes.is_empty());
let body = Bytes::from_static(b"123");
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123"[..]);
}
#[actix_rt::test]
async fn to_bytes_streams() {
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
.map(Ok::<_, Error>);
let body = BodyStream::new(stream);
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123abc"[..]);
}
#[actix_rt::test]
async fn to_bytes_limited_complete() {
let bytes = to_bytes_limited((), 0).await.unwrap().unwrap();
assert!(bytes.is_empty());
let bytes = to_bytes_limited((), 1).await.unwrap().unwrap();
assert!(bytes.is_empty());
assert!(to_bytes_limited(Bytes::from_static(b"12"), 0)
.await
.is_err());
assert!(to_bytes_limited(Bytes::from_static(b"12"), 1)
.await
.is_err());
assert!(to_bytes_limited(Bytes::from_static(b"12"), 2).await.is_ok());
assert!(to_bytes_limited(Bytes::from_static(b"12"), 3).await.is_ok());
}
#[actix_rt::test]
async fn to_bytes_limited_streams() {
// hinting a larger body fails
let body = SizedStream::new(8, stream::empty().map(Ok::<_, Error>));
assert!(to_bytes_limited(body, 3).await.is_err());
// hinting a smaller body is okay
let body = SizedStream::new(3, stream::empty().map(Ok::<_, Error>));
assert!(to_bytes_limited(body, 3).await.unwrap().unwrap().is_empty());
// hinting a smaller body then returning a larger one fails
let stream = stream::iter(vec![Bytes::from_static(b"1234")]).map(Ok::<_, Error>);
let body = SizedStream::new(3, stream);
assert!(to_bytes_limited(body, 3).await.is_err());
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
.map(Ok::<_, Error>);
let body = BodyStream::new(stream);
assert!(to_bytes_limited(body, 3).await.is_err());
}
#[actix_rt::test]
async fn to_body_limit_error() {
let err_stream = stream::once(async { Err(io::Error::new(io::ErrorKind::Other, "")) });
let body = SizedStream::new(8, err_stream);
// not too big, but propagates error from body stream
assert!(to_bytes_limited(body, 10).await.unwrap().is_err());
}
}

View File

@@ -211,7 +211,6 @@ where
/// Finish service configuration and create a service for the HTTP/2 protocol.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
where
F: IntoServiceFactory<S, Request>,

View File

@@ -161,44 +161,44 @@ impl From<crate::ws::ProtocolError> for Error {
#[non_exhaustive]
pub enum ParseError {
/// An invalid `Method`, such as `GE.T`.
#[display(fmt = "Invalid Method specified")]
#[display(fmt = "invalid method specified")]
Method,
/// An invalid `Uri`, such as `exam ple.domain`.
#[display(fmt = "Uri error: {}", _0)]
#[display(fmt = "URI error: {}", _0)]
Uri(InvalidUri),
/// An invalid `HttpVersion`, such as `HTP/1.1`
#[display(fmt = "Invalid HTTP version specified")]
#[display(fmt = "invalid HTTP version specified")]
Version,
/// An invalid `Header`.
#[display(fmt = "Invalid Header provided")]
#[display(fmt = "invalid Header provided")]
Header,
/// A message head is too large to be reasonable.
#[display(fmt = "Message head is too large")]
#[display(fmt = "message head is too large")]
TooLarge,
/// A message reached EOF, but is not complete.
#[display(fmt = "Message is incomplete")]
#[display(fmt = "message is incomplete")]
Incomplete,
/// An invalid `Status`, such as `1337 ELITE`.
#[display(fmt = "Invalid Status provided")]
#[display(fmt = "invalid status provided")]
Status,
/// A timeout occurred waiting for an IO event.
#[allow(dead_code)]
#[display(fmt = "Timeout")]
#[display(fmt = "timeout")]
Timeout,
/// An `io::Error` that occurred while trying to read or write to a network stream.
#[display(fmt = "IO error: {}", _0)]
/// An I/O error that occurred while trying to read or write to a network stream.
#[display(fmt = "I/O error: {}", _0)]
Io(io::Error),
/// Parsing a field as string failed.
#[display(fmt = "UTF8 error: {}", _0)]
#[display(fmt = "UTF-8 error: {}", _0)]
Utf8(Utf8Error),
}
@@ -257,22 +257,19 @@ impl From<ParseError> for Response<BoxBody> {
#[non_exhaustive]
pub enum PayloadError {
/// A payload reached EOF, but is not complete.
#[display(
fmt = "A payload reached EOF, but is not complete. Inner error: {:?}",
_0
)]
#[display(fmt = "payload reached EOF before completing: {:?}", _0)]
Incomplete(Option<io::Error>),
/// Content encoding stream corruption.
#[display(fmt = "Can not decode content-encoding.")]
#[display(fmt = "can not decode content-encoding")]
EncodingCorrupted,
/// Payload reached size limit.
#[display(fmt = "Payload reached size limit.")]
#[display(fmt = "payload reached size limit")]
Overflow,
/// Payload length is unknown.
#[display(fmt = "Payload length is unknown.")]
#[display(fmt = "payload length is unknown")]
UnknownLength,
/// HTTP/2 payload error.
@@ -294,7 +291,6 @@ impl std::error::Error for PayloadError {
PayloadError::Overflow => None,
PayloadError::UnknownLength => None,
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
PayloadError::Http2Payload(err) => Some(err),
PayloadError::Io(err) => Some(err),
}
@@ -331,44 +327,44 @@ impl From<PayloadError> for Error {
#[non_exhaustive]
pub enum DispatchError {
/// Service error.
#[display(fmt = "Service Error")]
#[display(fmt = "service error")]
Service(Response<BoxBody>),
/// Body streaming error.
#[display(fmt = "Body error: {}", _0)]
#[display(fmt = "body error: {}", _0)]
Body(Box<dyn StdError>),
/// Upgrade service error.
#[display(fmt = "upgrade error")]
Upgrade,
/// An `io::Error` that occurred while trying to read or write to a network stream.
#[display(fmt = "IO error: {}", _0)]
#[display(fmt = "I/O error: {}", _0)]
Io(io::Error),
/// Request parse error.
#[display(fmt = "Request parse error: {}", _0)]
#[display(fmt = "request parse error: {}", _0)]
Parse(ParseError),
/// HTTP/2 error.
#[display(fmt = "{}", _0)]
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
H2(h2::Error),
/// The first request did not complete within the specified timeout.
#[display(fmt = "The first request did not complete within the specified timeout")]
#[display(fmt = "request did not complete within the specified timeout")]
SlowRequestTimeout,
/// Disconnect timeout. Makes sense for ssl streams.
#[display(fmt = "Connection shutdown timeout")]
/// Disconnect timeout. Makes sense for TLS streams.
#[display(fmt = "connection shutdown timeout")]
DisconnectTimeout,
/// Handler dropped payload before reading EOF.
#[display(fmt = "Handler dropped payload before reading EOF")]
#[display(fmt = "handler dropped payload before reading EOF")]
HandlerDroppedPayload,
/// Internal error.
#[display(fmt = "Internal error")]
#[display(fmt = "internal error")]
InternalError,
}
@@ -393,12 +389,12 @@ impl StdError for DispatchError {
#[cfg_attr(test, derive(PartialEq, Eq))]
#[non_exhaustive]
pub enum ContentTypeError {
/// Can not parse content type
#[display(fmt = "Can not parse content type")]
/// Can not parse content type.
#[display(fmt = "could not parse content type")]
ParseError,
/// Unknown content encoding
#[display(fmt = "Unknown content encoding")]
/// Unknown content encoding.
#[display(fmt = "unknown content encoding")]
UnknownEncoding,
}
@@ -426,7 +422,7 @@ mod tests {
let err: Error = ParseError::Io(orig).into();
assert_eq!(
format!("{}", err),
"error parsing HTTP message: IO error: other"
"error parsing HTTP message: I/O error: other"
);
}
@@ -453,7 +449,7 @@ mod tests {
let err = PayloadError::Incomplete(None);
assert_eq!(
err.to_string(),
"A payload reached EOF, but is not complete. Inner error: None"
"payload reached EOF before completing: None"
);
}
@@ -473,7 +469,7 @@ mod tests {
match ParseError::from($from) {
e @ $error => {
let desc = format!("{}", e);
assert_eq!(desc, format!("IO error: {}", $from));
assert_eq!(desc, format!("I/O error: {}", $from));
}
_ => unreachable!("{:?}", $from),
}

View File

@@ -932,7 +932,6 @@ fn http_msg(msg: impl AsRef<str>) -> BytesMut {
.as_ref()
.trim()
.split('\n')
.into_iter()
.map(|line| [line.trim_start(), "\r"].concat())
.collect::<Vec<_>>()
.join("\n");

View File

@@ -134,7 +134,6 @@ mod openssl {
U::InitError: fmt::Debug,
{
/// Create OpenSSL based service.
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
pub fn openssl(
self,
acceptor: SslAcceptor,
@@ -197,7 +196,6 @@ mod rustls {
U::InitError: fmt::Debug,
{
/// Create Rustls based service.
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn rustls(
self,
config: ServerConfig,

View File

@@ -117,7 +117,6 @@ mod openssl {
B: MessageBody + 'static,
{
/// Create OpenSSL based service.
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
pub fn openssl(
self,
acceptor: SslAcceptor,
@@ -165,7 +164,6 @@ mod rustls {
B: MessageBody + 'static,
{
/// Create Rustls based service.
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn rustls(
self,
mut config: ServerConfig,

View File

@@ -8,12 +8,14 @@ use http::header::HeaderName;
/// request.
///
/// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics.
// TODO(breaking): replace with http's version
pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status");
/// Response header field that allows origin servers to control the behavior of CDN caches
/// interposed between them and clients separately from other caches that might handle the response.
///
/// See [RFC 9213](https://www.rfc-editor.org/rfc/rfc9213) for full semantics.
// TODO(breaking): replace with http's version
pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control");
/// Response header that prevents a document from loading any cross-origin resources that don't

View File

@@ -26,7 +26,7 @@
)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use ::http::{uri, uri::Uri};
pub use ::http::{Method, StatusCode, Version};
@@ -41,7 +41,6 @@ pub mod error;
mod extensions;
pub mod h1;
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub mod h2;
pub mod header;
mod helpers;
@@ -56,7 +55,6 @@ mod responses;
mod service;
pub mod test;
#[cfg(feature = "ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
pub mod ws;
pub use self::builder::HttpServiceBuilder;
@@ -74,7 +72,6 @@ pub use self::requests::{Request, RequestHead, RequestHeadType};
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
pub use self::service::HttpService;
#[cfg(any(feature = "openssl", feature = "rustls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
pub use self::service::TlsAcceptorConfig;
/// A major HTTP protocol version.

View File

@@ -217,7 +217,6 @@ where
/// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2
/// on plaintext connections.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn tcp_auto_h2c(
self,
) -> impl ServiceFactory<
@@ -253,7 +252,6 @@ where
/// Configuration options used when accepting TLS connection.
#[cfg(any(feature = "openssl", feature = "rustls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
#[derive(Debug, Default)]
pub struct TlsAcceptorConfig {
pub(crate) handshake_timeout: Option<std::time::Duration>,
@@ -309,7 +307,6 @@ mod openssl {
U::InitError: fmt::Debug,
{
/// Create OpenSSL based service.
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
pub fn openssl(
self,
acceptor: SslAcceptor,
@@ -324,7 +321,6 @@ mod openssl {
}
/// Create OpenSSL based service with custom TLS acceptor configuration.
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
pub fn openssl_with_config(
self,
acceptor: SslAcceptor,
@@ -404,7 +400,6 @@ mod rustls {
U::InitError: fmt::Debug,
{
/// Create Rustls based service.
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn rustls(
self,
config: ServerConfig,
@@ -419,7 +414,6 @@ mod rustls {
}
/// Create Rustls based service with custom TLS acceptor configuration.
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn rustls_with_config(
self,
mut config: ServerConfig,

View File

@@ -26,39 +26,39 @@ pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode};
#[derive(Debug, Display, Error, From)]
pub enum ProtocolError {
/// Received an unmasked frame from client.
#[display(fmt = "Received an unmasked frame from client.")]
#[display(fmt = "received an unmasked frame from client")]
UnmaskedFrame,
/// Received a masked frame from server.
#[display(fmt = "Received a masked frame from server.")]
#[display(fmt = "received a masked frame from server")]
MaskedFrame,
/// Encountered invalid opcode.
#[display(fmt = "Invalid opcode: {}.", _0)]
#[display(fmt = "invalid opcode ({})", _0)]
InvalidOpcode(#[error(not(source))] u8),
/// Invalid control frame length
#[display(fmt = "Invalid control frame length: {}.", _0)]
#[display(fmt = "invalid control frame length ({})", _0)]
InvalidLength(#[error(not(source))] usize),
/// Bad opcode.
#[display(fmt = "Bad opcode.")]
#[display(fmt = "bad opcode")]
BadOpCode,
/// A payload reached size limit.
#[display(fmt = "A payload reached size limit.")]
#[display(fmt = "payload reached size limit")]
Overflow,
/// Continuation is not started.
#[display(fmt = "Continuation is not started.")]
/// Continuation has not started.
#[display(fmt = "continuation has not started")]
ContinuationNotStarted,
/// Received new continuation but it is already started.
#[display(fmt = "Received new continuation but it is already started.")]
#[display(fmt = "received new continuation but it has already started")]
ContinuationStarted,
/// Unknown continuation fragment.
#[display(fmt = "Unknown continuation fragment: {}.", _0)]
#[display(fmt = "unknown continuation fragment: {}", _0)]
ContinuationFragment(#[error(not(source))] OpCode),
/// I/O error.
@@ -70,27 +70,27 @@ pub enum ProtocolError {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
pub enum HandshakeError {
/// Only get method is allowed.
#[display(fmt = "Method not allowed.")]
#[display(fmt = "method not allowed")]
GetMethodRequired,
/// Upgrade header if not set to WebSocket.
#[display(fmt = "WebSocket upgrade is expected.")]
#[display(fmt = "WebSocket upgrade is expected")]
NoWebsocketUpgrade,
/// Connection header is not set to upgrade.
#[display(fmt = "Connection upgrade is expected.")]
#[display(fmt = "connection upgrade is expected")]
NoConnectionUpgrade,
/// WebSocket version header is not set.
#[display(fmt = "WebSocket version header is required.")]
#[display(fmt = "WebSocket version header is required")]
NoVersionHeader,
/// Unsupported WebSocket version.
#[display(fmt = "Unsupported WebSocket version.")]
#[display(fmt = "unsupported WebSocket version")]
UnsupportedVersion,
/// WebSocket key is not set or wrong.
#[display(fmt = "Unknown websocket key.")]
#[display(fmt = "unknown WebSocket key")]
BadWebsocketKey,
}

View File

@@ -39,13 +39,13 @@ impl WsService {
#[derive(Debug, Display, Error, From)]
enum WsServiceError {
#[display(fmt = "http error")]
#[display(fmt = "HTTP error")]
Http(actix_http::Error),
#[display(fmt = "ws handshake error")]
#[display(fmt = "WS handshake error")]
Ws(actix_http::ws::HandshakeError),
#[display(fmt = "io error")]
#[display(fmt = "I/O error")]
Io(std::io::Error),
#[display(fmt = "dispatcher error")]

View File

@@ -0,0 +1,5 @@
# Changes
## 0.6.0 - 2023-02-26
- Add `MultipartForm` derive macro.

View File

@@ -0,0 +1,30 @@
[package]
name = "actix-multipart-derive"
version = "0.6.0"
authors = ["Jacob Halsey <jacob@jhalsey.com>"]
description = "Multipart form derive macro 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 = "2018"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[lib]
proc-macro = true
[dependencies]
darling = "0.14"
parse-size = "1"
proc-macro2 = "1"
quote = "1"
syn = "1"
[dev-dependencies]
actix-multipart = "0.6"
actix-web = "4"
rustversion = "1"
trybuild = "1"

View File

@@ -0,0 +1 @@
../LICENSE-APACHE

View File

@@ -0,0 +1 @@
../LICENSE-MIT

View File

@@ -0,0 +1,17 @@
# actix-multipart-derive
> The derive macro implementation for actix-multipart-derive.
[![crates.io](https://img.shields.io/crates/v/actix-multipart-derive?label=latest)](https://crates.io/crates/actix-multipart-derive)
[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.5.0)](https://docs.rs/actix-multipart-derive/0.5.0)
![Version](https://img.shields.io/badge/rustc-1.59+-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.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)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-multipart-derive)
- Minimum Supported Rust Version (MSRV): 1.59

View File

@@ -0,0 +1,315 @@
//! Multipart form derive macro for Actix Web.
//!
//! 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))]
use std::{collections::HashSet, convert::TryFrom as _};
use darling::{FromDeriveInput, FromField, FromMeta};
use parse_size::parse_size;
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
use syn::{parse_macro_input, Type};
#[derive(FromMeta)]
enum DuplicateField {
Ignore,
Deny,
Replace,
}
impl Default for DuplicateField {
fn default() -> Self {
Self::Ignore
}
}
#[derive(FromDeriveInput, Default)]
#[darling(attributes(multipart), default)]
struct MultipartFormAttrs {
deny_unknown_fields: bool,
duplicate_field: DuplicateField,
}
#[derive(FromField, Default)]
#[darling(attributes(multipart), default)]
struct FieldAttrs {
rename: Option<String>,
limit: Option<String>,
}
struct ParsedField<'t> {
serialization_name: String,
rust_name: &'t Ident,
limit: Option<usize>,
ty: &'t Type,
}
/// Implements `MultipartCollect` for a struct so that it can be used with the `MultipartForm`
/// extractor.
///
/// # Basic Use
///
/// Each field type should implement the `FieldReader` trait:
///
/// ```
/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
///
/// #[derive(MultipartForm)]
/// struct ImageUpload {
/// description: Text<String>,
/// timestamp: Text<i64>,
/// image: TempFile,
/// }
/// ```
///
/// # Optional and List Fields
///
/// You can also use `Vec<T>` and `Option<T>` provided that `T: FieldReader`.
///
/// A [`Vec`] field corresponds to an upload with multiple parts under the [same field
/// name](https://www.rfc-editor.org/rfc/rfc7578#section-4.3).
///
/// ```
/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
///
/// #[derive(MultipartForm)]
/// struct Form {
/// category: Option<Text<String>>,
/// files: Vec<TempFile>,
/// }
/// ```
///
/// # Field Renaming
///
/// You can use the `#[multipart(rename = "foo")]` attribute to receive a field by a different name.
///
/// ```
/// use actix_multipart::form::{tempfile::TempFile, MultipartForm};
///
/// #[derive(MultipartForm)]
/// struct Form {
/// #[multipart(rename = "files[]")]
/// files: Vec<TempFile>,
/// }
/// ```
///
/// # Field Limits
///
/// You can use the `#[multipart(limit = "<size>")]` attribute to set field level limits. The limit
/// string is parsed using [parse_size].
///
/// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
///
/// ```
/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
///
/// #[derive(MultipartForm)]
/// struct Form {
/// #[multipart(limit = "2 KiB")]
/// description: Text<String>,
///
/// #[multipart(limit = "512 MiB")]
/// files: Vec<TempFile>,
/// }
/// ```
///
/// # Unknown Fields
///
/// By default fields with an unknown name are ignored. They can be rejected using the
/// `#[multipart(deny_unknown_fields)]` attribute:
///
/// ```
/// # use actix_multipart::form::MultipartForm;
/// #[derive(MultipartForm)]
/// #[multipart(deny_unknown_fields)]
/// struct Form { }
/// ```
///
/// # Duplicate Fields
///
/// The behaviour for when multiple fields with the same name are received can be changed using the
/// `#[multipart(duplicate_field = "<behavior>")]` attribute:
///
/// - "ignore": (default) Extra fields are ignored. I.e., the first one is persisted.
/// - "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.
///
/// ```
/// # use actix_multipart::form::MultipartForm;
/// #[derive(MultipartForm)]
/// #[multipart(duplicate_field = "deny")]
/// struct Form { }
/// ```
///
/// [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);
let name = &input.ident;
let data_struct = match &input.data {
syn::Data::Struct(data_struct) => data_struct,
_ => {
return compile_err(syn::Error::new(
input.ident.span(),
"`MultipartForm` can only be derived for structs",
))
}
};
let fields = match &data_struct.fields {
syn::Fields::Named(fields_named) => fields_named,
_ => {
return compile_err(syn::Error::new(
input.ident.span(),
"`MultipartForm` can only be derived for a struct with named fields",
))
}
};
let attrs = match MultipartFormAttrs::from_derive_input(&input) {
Ok(attrs) => attrs,
Err(err) => return err.write_errors().into(),
};
// Parse the field attributes
let parsed = match fields
.named
.iter()
.map(|field| {
let rust_name = field.ident.as_ref().unwrap();
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 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),
)),
}) {
Some(Err(err)) => return Err(compile_err(err)),
limit => limit.map(Result::unwrap),
};
Ok(ParsedField {
serialization_name,
rust_name,
limit,
ty: &field.ty,
})
})
.collect::<Result<Vec<_>, TokenStream>>()
{
Ok(attrs) => attrs,
Err(err) => return err,
};
// Check that field names are unique
let mut set = HashSet::new();
for field in &parsed {
if !set.insert(field.serialization_name.clone()) {
return compile_err(syn::Error::new(
field.rust_name.span(),
format!("Multiple fields named: `{}`", field.serialization_name),
));
}
}
// 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::UnsupportedField(field.name().to_string())
))
} else {
quote!(::std::result::Result::Ok(()))
};
// Value for duplicate action
let duplicate_field = match attrs.duplicate_field {
DuplicateField::Ignore => quote!(::actix_multipart::form::DuplicateField::Ignore),
DuplicateField::Deny => quote!(::actix_multipart::form::DuplicateField::Deny),
DuplicateField::Replace => quote!(::actix_multipart::form::DuplicateField::Replace),
};
// limit() implementation
let mut limit_impl = quote!();
for field in &parsed {
let name = &field.serialization_name;
if let Some(value) = field.limit {
limit_impl.extend(quote!(
#name => ::std::option::Option::Some(#value),
));
}
}
// handle_field() implementation
let mut handle_field_impl = quote!();
for field in &parsed {
let name = &field.serialization_name;
let ty = &field.ty;
handle_field_impl.extend(quote!(
#name => ::std::boxed::Box::pin(
<#ty as ::actix_multipart::form::FieldGroupReader>::handle_field(req, field, limits, state, #duplicate_field)
),
));
}
// from_state() implementation
let mut from_state_impl = quote!();
for field in &parsed {
let name = &field.serialization_name;
let rust_name = &field.rust_name;
let ty = &field.ty;
from_state_impl.extend(quote!(
#rust_name: <#ty as ::actix_multipart::form::FieldGroupReader>::from_state(#name, &mut state)?,
));
}
let gen = quote! {
impl ::actix_multipart::form::MultipartCollect for #name {
fn limit(field_name: &str) -> ::std::option::Option<usize> {
match field_name {
#limit_impl
_ => None,
}
}
fn handle_field<'t>(
req: &'t ::actix_web::HttpRequest,
field: ::actix_multipart::Field,
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() {
#handle_field_impl
_ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)),
}
}
fn from_state(mut state: ::actix_multipart::form::State) -> ::std::result::Result<Self, ::actix_multipart::MultipartError> {
Ok(Self {
#from_state_impl
})
}
}
};
gen.into()
}
/// Transform a syn error into a token stream for returning.
fn compile_err(err: syn::Error) -> TokenStream {
TokenStream::from(err.to_compile_error())
}

View File

@@ -0,0 +1,16 @@
#[rustversion::stable(1.59)] // MSRV
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();
t.pass("tests/trybuild/all-required.rs");
t.pass("tests/trybuild/optional-and-list.rs");
t.pass("tests/trybuild/rename.rs");
t.pass("tests/trybuild/deny-unknown.rs");
t.pass("tests/trybuild/deny-duplicates.rs");
t.compile_fail("tests/trybuild/deny-parse-fail.rs");
t.pass("tests/trybuild/size-limits.rs");
t.compile_fail("tests/trybuild/size-limit-parse-fail.rs");
}

View File

@@ -0,0 +1,19 @@
use actix_web::{web, App, Responder};
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
#[derive(Debug, MultipartForm)]
struct ImageUpload {
description: Text<String>,
timestamp: Text<i64>,
image: TempFile,
}
async fn handler(_form: MultipartForm<ImageUpload>) -> impl Responder {
"Hello World!"
}
#[actix_web::main]
async fn main() {
App::new().default_service(web::to(handler));
}

View File

@@ -0,0 +1,16 @@
use actix_web::{web, App, Responder};
use actix_multipart::form::MultipartForm;
#[derive(MultipartForm)]
#[multipart(duplicate_field = "deny")]
struct Form {}
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
"Hello World!"
}
#[actix_web::main]
async fn main() {
App::new().default_service(web::to(handler));
}

View File

@@ -0,0 +1,7 @@
use actix_multipart::form::MultipartForm;
#[derive(MultipartForm)]
#[multipart(duplicate_field = "no")]
struct Form {}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: Unknown literal value `no`
--> tests/trybuild/deny-parse-fail.rs:4:31
|
4 | #[multipart(duplicate_field = "no")]
| ^^^^

View File

@@ -0,0 +1,16 @@
use actix_web::{web, App, Responder};
use actix_multipart::form::MultipartForm;
#[derive(MultipartForm)]
#[multipart(deny_unknown_fields)]
struct Form {}
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
"Hello World!"
}
#[actix_web::main]
async fn main() {
App::new().default_service(web::to(handler));
}

View File

@@ -0,0 +1,18 @@
use actix_web::{web, App, Responder};
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
#[derive(MultipartForm)]
struct Form {
category: Option<Text<String>>,
files: Vec<TempFile>,
}
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
"Hello World!"
}
#[actix_web::main]
async fn main() {
App::new().default_service(web::to(handler));
}

View File

@@ -0,0 +1,18 @@
use actix_web::{web, App, Responder};
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
#[derive(MultipartForm)]
struct Form {
#[multipart(rename = "files[]")]
files: Vec<TempFile>,
}
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
"Hello World!"
}
#[actix_web::main]
async fn main() {
App::new().default_service(web::to(handler));
}

View File

@@ -0,0 +1,21 @@
use actix_multipart::form::{text::Text, MultipartForm};
#[derive(MultipartForm)]
struct Form {
#[multipart(limit = "2 bytes")]
description: Text<String>,
}
#[derive(MultipartForm)]
struct Form2 {
#[multipart(limit = "2 megabytes")]
description: Text<String>,
}
#[derive(MultipartForm)]
struct Form3 {
#[multipart(limit = "four meters")]
description: Text<String>,
}
fn main() {}

View File

@@ -0,0 +1,17 @@
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`: 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`: invalid digit found in string
--> tests/trybuild/size-limit-parse-fail.rs:18:5
|
18 | description: Text<String>,
| ^^^^^^^^^^^

View File

@@ -0,0 +1,21 @@
use actix_web::{web, App, Responder};
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
#[derive(MultipartForm)]
struct Form {
#[multipart(limit = "2 KiB")]
description: Text<String>,
#[multipart(limit = "512 MiB")]
files: Vec<TempFile>,
}
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
"Hello World!"
}
#[actix_web::main]
async fn main() {
App::new().default_service(web::to(handler));
}

View File

@@ -1,39 +1,48 @@
# Changes
## Unreleased - 2022-xx-xx
## Unreleased - 2023-xx-xx
## 0.6.0 - 2023-02-26
- Added `MultipartForm` typed data extractor. [#2883]
[#2883]: https://github.com/actix/actix-web/pull/2883
## 0.5.0 - 2023-01-21
- `Field::content_type()` now returns `Option<&mime::Mime>`. [#2885]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
- `Field::content_type()` now returns `Option<&mime::Mime>` [#2880]
[#2880]: https://github.com/actix/actix-web/pull/2880
[#2885]: https://github.com/actix/actix-web/pull/2885
## 0.4.0 - 2022-02-25
- No significant changes since `0.4.0-beta.13`.
## 0.4.0-beta.13 - 2022-01-31
- No significant changes since `0.4.0-beta.12`.
## 0.4.0-beta.12 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.
## 0.4.0-beta.11 - 2021-12-27
- No significant changes since `0.4.0-beta.10`.
## 0.4.0-beta.10 - 2021-12-11
- No significant changes since `0.4.0-beta.9`.
## 0.4.0-beta.9 - 2021-12-01
- Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463]
[#2463]: https://github.com/actix/actix-web/pull/2463
## 0.4.0-beta.8 - 2021-11-22
- Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451]
- Added `MultipartError::NoContentDisposition` variant. [#2451]
- Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451]
@@ -43,52 +52,52 @@
[#2451]: https://github.com/actix/actix-web/pull/2451
## 0.4.0-beta.7 - 2021-10-20
- Minimum supported Rust version (MSRV) is now 1.52.
## 0.4.0-beta.6 - 2021-09-09
- Minimum supported Rust version (MSRV) is now 1.51.
## 0.4.0-beta.5 - 2021-06-17
- No notable changes.
- No notable changes.
## 0.4.0-beta.4 - 2021-04-02
- No notable changes.
- No notable changes.
## 0.4.0-beta.3 - 2021-03-09
- No notable changes.
- No notable changes.
## 0.4.0-beta.2 - 2021-02-10
- No notable changes.
## 0.4.0-beta.1 - 2021-01-07
- Fix multipart consuming payload before header checks. [#1513]
- Update `bytes` to `1.0`. [#1813]
[#1813]: https://github.com/actix/actix-web/pull/1813
[#1513]: https://github.com/actix/actix-web/pull/1513
## 0.3.0 - 2020-09-11
- No significant changes from `0.3.0-beta.2`.
## 0.3.0-beta.2 - 2020-09-10
- Update `actix-*` dependencies to latest versions.
## 0.3.0-beta.1 - 2020-07-15
- Update `actix-web` to 3.0.0-beta.1
## 0.3.0-alpha.1 - 2020-05-25
- Update `actix-web` to 3.0.0-alpha.3
- Bump minimum supported Rust version to 1.40
- Minimize `futures` dependencies

View File

@@ -1,7 +1,10 @@
[package]
name = "actix-multipart"
version = "0.4.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
version = "0.6.0"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Jacob Halsey <jacob@jhalsey.com>",
]
description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
@@ -9,26 +12,42 @@ repository = "https://github.com/actix/actix-web.git"
license = "MIT OR Apache-2.0"
edition = "2018"
[lib]
name = "actix_multipart"
path = "src/lib.rs"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[features]
default = ["tempfile", "derive"]
derive = ["actix-multipart-derive"]
tempfile = ["tempfile-dep", "tokio/fs"]
[dependencies]
actix-multipart-derive = { version = "=0.6.0", optional = true }
actix-utils = "3"
actix-web = { version = "4", default-features = false }
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"
local-waker = "0.1"
log = "0.4"
mime = "0.3"
memchr = "2.5"
mime = "0.3"
serde = "1"
serde_json = "1"
serde_plain = "1"
# TODO(MSRV 1.60): replace with dep: prefix
tempfile-dep = { package = "tempfile", version = "3.4", optional = true }
tokio = { version = "1.24.2", features = ["sync"] }
[dev-dependencies]
actix-rt = "2.2"
actix-http = "3"
actix-multipart-rfc7578 = "0.10"
actix-rt = "2.2"
actix-test = "0.1"
awc = "3"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
tokio = { version = "1.18.4", features = ["sync"] }
tokio = { version = "1.24.2", features = ["sync"] }
tokio-stream = "0.1"

View File

@@ -3,15 +3,15 @@
> 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.4.0)](https://docs.rs/actix-multipart/0.4.0)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.6.0)](https://docs.rs/actix-multipart/0.6.0)
![Version](https://img.shields.io/badge/rustc-1.59+-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.4.0/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0)
[![dependency status](https://deps.rs/crate/actix-multipart/0.6.0/status.svg)](https://deps.rs/crate/actix-multipart/0.6.0)
[![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)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-multipart)
- Minimum Supported Rust Version (MSRV): 1.54
- Minimum Supported Rust Version (MSRV): 1.59

View File

@@ -1,12 +1,15 @@
//! Error and Result module
use actix_web::error::{ParseError, PayloadError};
use actix_web::http::StatusCode;
use actix_web::ResponseError;
use actix_web::{
error::{ParseError, PayloadError},
http::StatusCode,
ResponseError,
};
use derive_more::{Display, Error, From};
/// A set of errors that can occur during parsing multipart streams
#[non_exhaustive]
/// A set of errors that can occur during parsing multipart streams.
#[derive(Debug, Display, From, Error)]
#[non_exhaustive]
pub enum MultipartError {
/// Content-Disposition header is not found or is not equal to "form-data".
///
@@ -46,12 +49,41 @@ pub enum MultipartError {
/// Not consumed
#[display(fmt = "Multipart stream is not consumed")]
NotConsumed,
/// An error from a field handler in a form
#[display(
fmt = "An error occurred processing field `{}`: {}",
field_name,
source
)]
Field {
field_name: String,
source: actix_web::Error,
},
/// Duplicate field
#[display(fmt = "Duplicate field found for: `{}`", _0)]
#[from(ignore)]
DuplicateField(#[error(not(source))] String),
/// Missing field
#[display(fmt = "Field with name `{}` is required", _0)]
#[from(ignore)]
MissingField(#[error(not(source))] String),
/// Unknown field
#[display(fmt = "Unsupported field `{}`", _0)]
#[from(ignore)]
UnsupportedField(#[error(not(source))] String),
}
/// Return `BadRequest` for `MultipartError`
impl ResponseError for MultipartError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
match &self {
MultipartError::Field { source, .. } => source.as_response_error().status_code(),
_ => StatusCode::BAD_REQUEST,
}
}
}

View File

@@ -9,8 +9,7 @@ use crate::server::Multipart;
///
/// Content-type: multipart/form-data;
///
/// ## Server example
///
/// # Examples
/// ```
/// use actix_web::{web, HttpResponse, Error};
/// use actix_multipart::Multipart;

View File

@@ -0,0 +1,53 @@
//! Reads a field into memory.
use actix_web::HttpRequest;
use bytes::BytesMut;
use futures_core::future::LocalBoxFuture;
use futures_util::TryStreamExt as _;
use mime::Mime;
use crate::{
form::{FieldReader, Limits},
Field, MultipartError,
};
/// Read the field into memory.
#[derive(Debug)]
pub struct Bytes {
/// The data.
pub data: bytes::Bytes,
/// The value of the `Content-Type` header.
pub content_type: Option<Mime>,
/// The `filename` value in the `Content-Disposition` header.
pub file_name: Option<String>,
}
impl<'t> FieldReader<'t> for Bytes {
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 {
let mut buf = BytesMut::with_capacity(131_072);
while let Some(chunk) = field.try_next().await? {
limits.try_consume_limits(chunk.len(), true)?;
buf.extend(chunk);
}
Ok(Bytes {
data: buf.freeze(),
content_type: field.content_type().map(ToOwned::to_owned),
file_name: field
.content_disposition()
.get_filename()
.map(str::to_owned),
})
})
}
}

View File

@@ -0,0 +1,195 @@
//! Deserializes a field as JSON.
use std::sync::Arc;
use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError};
use derive_more::{Deref, DerefMut, Display, Error};
use futures_core::future::LocalBoxFuture;
use serde::de::DeserializeOwned;
use crate::{
form::{bytes::Bytes, FieldReader, Limits},
Field, MultipartError,
};
use super::FieldErrorHandler;
/// Deserialize from JSON.
#[derive(Debug, Deref, DerefMut)]
pub struct Json<T: DeserializeOwned>(pub T);
impl<T: DeserializeOwned> Json<T> {
pub fn into_inner(self) -> T {
self.0
}
}
impl<'t, T> FieldReader<'t> for Json<T>
where
T: DeserializeOwned + 'static,
{
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
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() {
mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON)
} else {
false
};
if !valid {
return Err(MultipartError::Field {
field_name,
source: config.map_error(req, JsonFieldError::ContentType),
});
}
}
let bytes = Bytes::read_field(req, field, limits).await?;
Ok(Json(serde_json::from_slice(bytes.data.as_ref()).map_err(
|err| MultipartError::Field {
field_name,
source: config.map_error(req, JsonFieldError::Deserialize(err)),
},
)?))
})
}
}
#[derive(Debug, Display, Error)]
#[non_exhaustive]
pub enum JsonFieldError {
/// Deserialize error.
#[display(fmt = "Json deserialize error: {}", _0)]
Deserialize(serde_json::Error),
/// Content type error.
#[display(fmt = "Content type error")]
ContentType,
}
impl ResponseError for JsonFieldError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// Configuration for the [`Json`] field reader.
#[derive(Clone)]
pub struct JsonConfig {
err_handler: FieldErrorHandler<JsonFieldError>,
validate_content_type: bool,
}
const DEFAULT_CONFIG: JsonConfig = JsonConfig {
err_handler: None,
validate_content_type: true,
};
impl JsonConfig {
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(JsonFieldError, &HttpRequest) -> Error + Send + Sync + 'static,
{
self.err_handler = Some(Arc::new(f));
self
}
/// Extract payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
/// back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or(&DEFAULT_CONFIG)
}
fn map_error(&self, req: &HttpRequest, err: JsonFieldError) -> Error {
if let Some(err_handler) = self.err_handler.as_ref() {
(*err_handler)(err, req)
} else {
err.into()
}
}
/// Sets whether or not the field must have a valid `Content-Type` header to be parsed.
pub fn validate_content_type(mut self, validate_content_type: bool) -> Self {
self.validate_content_type = validate_content_type;
self
}
}
impl Default for JsonConfig {
fn default() -> Self {
DEFAULT_CONFIG
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, io::Cursor};
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,
};
#[derive(MultipartForm)]
struct JsonForm {
json: Json<HashMap<String, String>>,
}
async fn test_json_route(form: MultipartForm<JsonForm>) -> impl Responder {
let mut expected = HashMap::new();
expected.insert("key1".to_owned(), "value1".to_owned());
expected.insert("key2".to_owned(), "value2".to_owned());
assert_eq!(&*form.json, &expected);
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_json_without_content_type() {
let srv = actix_test::start(|| {
App::new()
.route("/", web::post().to(test_json_route))
.app_data(JsonConfig::default().validate_content_type(false))
});
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]
async fn test_content_type_validation() {
let srv = actix_test::start(|| {
App::new()
.route("/", web::post().to(test_json_route))
.app_data(JsonConfig::default().validate_content_type(true))
});
// Deny because wrong content type
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 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

@@ -0,0 +1,742 @@
//! Process and extract typed data from a multipart stream.
use std::{
any::Any,
collections::HashMap,
future::{ready, Future},
sync::Arc,
};
use actix_web::{dev, error::PayloadError, web, Error, FromRequest, HttpRequest};
use derive_more::{Deref, DerefMut};
use futures_core::future::LocalBoxFuture;
use futures_util::{TryFutureExt as _, TryStreamExt as _};
use crate::{Field, Multipart, MultipartError};
pub mod bytes;
pub mod json;
#[cfg(feature = "tempfile")]
pub mod tempfile;
pub mod text;
#[cfg(feature = "derive")]
pub use actix_multipart_derive::MultipartForm;
type FieldErrorHandler<T> = Option<Arc<dyn Fn(T, &HttpRequest) -> Error + Send + Sync>>;
/// Trait that data types to be used in a multipart form struct should implement.
///
/// It represents an asynchronous handler that processes a multipart field to produce `Self`.
pub trait FieldReader<'t>: Sized + Any {
/// Future that resolves to a `Self`.
type Future: Future<Output = Result<Self, MultipartError>>;
/// The form will call this function to handle the field.
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future;
}
/// Used to accumulate the state of the loaded fields.
#[doc(hidden)]
#[derive(Default, Deref, DerefMut)]
pub struct State(pub HashMap<String, Box<dyn Any>>);
/// Trait that the field collection types implement, i.e. `Vec<T>`, `Option<T>`, or `T` itself.
#[doc(hidden)]
pub trait FieldGroupReader<'t>: Sized + Any {
type Future: Future<Output = Result<(), MultipartError>>;
/// The form will call this function for each matching field.
fn handle_field(
req: &'t HttpRequest,
field: Field,
limits: &'t mut Limits,
state: &'t mut State,
duplicate_field: DuplicateField,
) -> Self::Future;
/// Construct `Self` from the group of processed fields.
fn from_state(name: &str, state: &'t mut State) -> Result<Self, MultipartError>;
}
impl<'t, T> FieldGroupReader<'t> for Option<T>
where
T: FieldReader<'t>,
{
type Future = LocalBoxFuture<'t, Result<(), MultipartError>>;
fn handle_field(
req: &'t HttpRequest,
field: Field,
limits: &'t mut Limits,
state: &'t mut State,
duplicate_field: DuplicateField,
) -> Self::Future {
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.name().to_owned(),
))))
}
DuplicateField::Replace => {}
}
}
Box::pin(async move {
let field_name = field.name().to_owned();
let t = T::read_field(req, field, limits).await?;
state.insert(field_name, Box::new(t));
Ok(())
})
}
fn from_state(name: &str, state: &'t mut State) -> Result<Self, MultipartError> {
Ok(state.remove(name).map(|m| *m.downcast::<T>().unwrap()))
}
}
impl<'t, T> FieldGroupReader<'t> for Vec<T>
where
T: FieldReader<'t>,
{
type Future = LocalBoxFuture<'t, Result<(), MultipartError>>;
fn handle_field(
req: &'t HttpRequest,
field: Field,
limits: &'t mut Limits,
state: &'t mut State,
_duplicate_field: DuplicateField,
) -> Self::Future {
Box::pin(async move {
// Note: Vec GroupReader always allows duplicates
let field_name = field.name().to_owned();
let vec = state
.entry(field_name)
.or_insert_with(|| Box::<Vec<T>>::default())
.downcast_mut::<Vec<T>>()
.unwrap();
let item = T::read_field(req, field, limits).await?;
vec.push(item);
Ok(())
})
}
fn from_state(name: &str, state: &'t mut State) -> Result<Self, MultipartError> {
Ok(state
.remove(name)
.map(|m| *m.downcast::<Vec<T>>().unwrap())
.unwrap_or_default())
}
}
impl<'t, T> FieldGroupReader<'t> for T
where
T: FieldReader<'t>,
{
type Future = LocalBoxFuture<'t, Result<(), MultipartError>>;
fn handle_field(
req: &'t HttpRequest,
field: Field,
limits: &'t mut Limits,
state: &'t mut State,
duplicate_field: DuplicateField,
) -> Self::Future {
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.name().to_owned(),
))))
}
DuplicateField::Replace => {}
}
}
Box::pin(async move {
let field_name = field.name().to_owned();
let t = T::read_field(req, field, limits).await?;
state.insert(field_name, Box::new(t));
Ok(())
})
}
fn from_state(name: &str, state: &'t mut State) -> Result<Self, MultipartError> {
state
.remove(name)
.map(|m| *m.downcast::<T>().unwrap())
.ok_or_else(|| MultipartError::MissingField(name.to_owned()))
}
}
/// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor.
///
/// You should use the [`macro@MultipartForm`] macro to derive this for your struct.
pub trait MultipartCollect: Sized {
/// An optional limit in bytes to be applied a given field name. Note this limit will be shared
/// across all fields sharing the same name.
fn limit(field_name: &str) -> Option<usize>;
/// The extractor will call this function for each incoming field, the state can be updated
/// with the processed field data.
fn handle_field<'t>(
req: &'t HttpRequest,
field: Field,
limits: &'t mut Limits,
state: &'t mut State,
) -> LocalBoxFuture<'t, Result<(), MultipartError>>;
/// Once all the fields have been processed and stored in the state, this is called
/// to convert into the struct representation.
fn from_state(state: State) -> Result<Self, MultipartError>;
}
#[doc(hidden)]
pub enum DuplicateField {
/// Additional fields are not processed.
Ignore,
/// An error will be raised.
Deny,
/// All fields will be processed, the last one will replace all previous.
Replace,
}
/// Used to keep track of the remaining limits for the form and current field.
pub struct Limits {
pub total_limit_remaining: usize,
pub memory_limit_remaining: usize,
pub field_limit_remaining: Option<usize>,
}
impl Limits {
pub fn new(total_limit: usize, memory_limit: usize) -> Self {
Self {
total_limit_remaining: total_limit,
memory_limit_remaining: memory_limit,
field_limit_remaining: None,
}
}
/// This function should be called within a [`FieldReader`] when reading each chunk of a field
/// to ensure that the form limits are not exceeded.
///
/// # Arguments
///
/// * `bytes` - The number of bytes being read from this chunk
/// * `in_memory` - Whether to consume from the memory limits
pub fn try_consume_limits(
&mut self,
bytes: usize,
in_memory: bool,
) -> Result<(), MultipartError> {
self.total_limit_remaining = self
.total_limit_remaining
.checked_sub(bytes)
.ok_or(MultipartError::Payload(PayloadError::Overflow))?;
if in_memory {
self.memory_limit_remaining = self
.memory_limit_remaining
.checked_sub(bytes)
.ok_or(MultipartError::Payload(PayloadError::Overflow))?;
}
if let Some(field_limit) = self.field_limit_remaining {
self.field_limit_remaining = Some(
field_limit
.checked_sub(bytes)
.ok_or(MultipartError::Payload(PayloadError::Overflow))?,
);
}
Ok(())
}
}
/// Typed `multipart/form-data` extractor.
///
/// To extract typed data from a multipart stream, the inner type `T` must implement the
/// [`MultipartCollect`] trait. You should use the [`macro@MultipartForm`] macro to derive this
/// for your struct.
///
/// Add a [`MultipartFormConfig`] to your app data to configure extraction.
#[derive(Deref, DerefMut)]
pub struct MultipartForm<T: MultipartCollect>(pub T);
impl<T: MultipartCollect> MultipartForm<T> {
/// Unwrap into inner `T` value.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> FromRequest for MultipartForm<T>
where
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 payload = Multipart::new(req.headers(), payload.take());
let config = MultipartFormConfig::from_req(req);
let mut limits = Limits::new(config.total_limit, config.memory_limit);
let req = req.clone();
let req2 = req.clone();
let err_handler = config.err_handler.clone();
Box::pin(
async move {
let mut state = State::default();
// 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) = payload.try_next().await? {
// Retrieve the limit for this field
let entry = field_limits
.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))
}
.map_err(move |err| {
if let Some(handler) = err_handler {
(*handler)(err, &req2)
} else {
err.into()
}
}),
)
}
}
type MultipartFormErrorHandler =
Option<Arc<dyn Fn(MultipartError, &HttpRequest) -> Error + Send + Sync>>;
/// [`struct@MultipartForm`] extractor configuration.
///
/// Add to your app data to have it picked up by [`struct@MultipartForm`] extractors.
#[derive(Clone)]
pub struct MultipartFormConfig {
total_limit: usize,
memory_limit: usize,
err_handler: MultipartFormErrorHandler,
}
impl MultipartFormConfig {
/// Sets maximum accepted payload size for the entire form. By default this limit is 50MiB.
pub fn total_limit(mut self, total_limit: usize) -> Self {
self.total_limit = total_limit;
self
}
/// Sets maximum accepted data that will be read into memory. By default this limit is 2MiB.
pub fn memory_limit(mut self, memory_limit: usize) -> Self {
self.memory_limit = memory_limit;
self
}
/// Sets custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(MultipartError, &HttpRequest) -> Error + Send + Sync + 'static,
{
self.err_handler = Some(Arc::new(f));
self
}
/// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
/// back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or(&DEFAULT_CONFIG)
}
}
const DEFAULT_CONFIG: MultipartFormConfig = MultipartFormConfig {
total_limit: 52_428_800, // 50 MiB
memory_limit: 2_097_152, // 2 MiB
err_handler: None,
};
impl Default for MultipartFormConfig {
fn default() -> Self {
DEFAULT_CONFIG
}
}
#[cfg(test)]
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, HttpResponse, Responder};
use awc::{Client, ClientResponse};
use super::MultipartForm;
use crate::form::{bytes::Bytes, tempfile::TempFile, text::Text, MultipartFormConfig};
pub async fn send_form(
srv: &TestServer,
form: multipart::Form<'static>,
uri: &'static str,
) -> ClientResponse<Decoder<Payload>> {
Client::default()
.post(srv.url(uri))
.content_type(form.content_type())
.send_body(multipart::Body::from(form))
.await
.unwrap()
}
/// Test `Option` fields.
#[derive(MultipartForm)]
struct TestOptions {
field1: Option<Text<String>>,
field2: Option<Text<String>>,
}
async fn test_options_route(form: MultipartForm<TestOptions>) -> impl Responder {
assert!(form.field1.is_some());
assert!(form.field2.is_none());
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_options() {
let srv =
actix_test::start(|| App::new().route("/", web::post().to(test_options_route)));
let mut form = multipart::Form::default();
form.add_text("field1", "value");
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
}
/// Test `Vec` fields.
#[derive(MultipartForm)]
struct TestVec {
list1: Vec<Text<String>>,
list2: Vec<Text<String>>,
}
async fn test_vec_route(form: MultipartForm<TestVec>) -> impl Responder {
let form = form.into_inner();
let strings = form
.list1
.into_iter()
.map(|s| s.into_inner())
.collect::<Vec<_>>();
assert_eq!(strings, vec!["value1", "value2", "value3"]);
assert_eq!(form.list2.len(), 0);
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_vec() {
let srv = actix_test::start(|| App::new().route("/", web::post().to(test_vec_route)));
let mut form = multipart::Form::default();
form.add_text("list1", "value1");
form.add_text("list1", "value2");
form.add_text("list1", "value3");
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
}
/// Test the `rename` field attribute.
#[derive(MultipartForm)]
struct TestFieldRenaming {
#[multipart(rename = "renamed")]
field1: Text<String>,
#[multipart(rename = "field1")]
field2: Text<String>,
field3: Text<String>,
}
async fn test_field_renaming_route(
form: MultipartForm<TestFieldRenaming>,
) -> impl Responder {
assert_eq!(&*form.field1, "renamed");
assert_eq!(&*form.field2, "field1");
assert_eq!(&*form.field3, "field3");
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_field_renaming() {
let srv = actix_test::start(|| {
App::new().route("/", web::post().to(test_field_renaming_route))
});
let mut form = multipart::Form::default();
form.add_text("renamed", "renamed");
form.add_text("field1", "field1");
form.add_text("field3", "field3");
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
}
/// Test the `deny_unknown_fields` struct attribute.
#[derive(MultipartForm)]
#[multipart(deny_unknown_fields)]
struct TestDenyUnknown {}
#[derive(MultipartForm)]
struct TestAllowUnknown {}
async fn test_deny_unknown_route(_: MultipartForm<TestDenyUnknown>) -> impl Responder {
HttpResponse::Ok().finish()
}
async fn test_allow_unknown_route(_: MultipartForm<TestAllowUnknown>) -> impl Responder {
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_deny_unknown() {
let srv = actix_test::start(|| {
App::new()
.route("/deny", web::post().to(test_deny_unknown_route))
.route("/allow", web::post().to(test_allow_unknown_route))
});
let mut form = multipart::Form::default();
form.add_text("unknown", "value");
let response = send_form(&srv, form, "/deny").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let mut form = multipart::Form::default();
form.add_text("unknown", "value");
let response = send_form(&srv, form, "/allow").await;
assert_eq!(response.status(), StatusCode::OK);
}
/// Test the `duplicate_field` struct attribute.
#[derive(MultipartForm)]
#[multipart(duplicate_field = "deny")]
struct TestDuplicateDeny {
_field: Text<String>,
}
#[derive(MultipartForm)]
#[multipart(duplicate_field = "replace")]
struct TestDuplicateReplace {
field: Text<String>,
}
#[derive(MultipartForm)]
#[multipart(duplicate_field = "ignore")]
struct TestDuplicateIgnore {
field: Text<String>,
}
async fn test_duplicate_deny_route(_: MultipartForm<TestDuplicateDeny>) -> impl Responder {
HttpResponse::Ok().finish()
}
async fn test_duplicate_replace_route(
form: MultipartForm<TestDuplicateReplace>,
) -> impl Responder {
assert_eq!(&*form.field, "second_value");
HttpResponse::Ok().finish()
}
async fn test_duplicate_ignore_route(
form: MultipartForm<TestDuplicateIgnore>,
) -> impl Responder {
assert_eq!(&*form.field, "first_value");
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_duplicate_field() {
let srv = actix_test::start(|| {
App::new()
.route("/deny", web::post().to(test_duplicate_deny_route))
.route("/replace", web::post().to(test_duplicate_replace_route))
.route("/ignore", web::post().to(test_duplicate_ignore_route))
});
let mut form = multipart::Form::default();
form.add_text("_field", "first_value");
form.add_text("_field", "second_value");
let response = send_form(&srv, form, "/deny").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let mut form = multipart::Form::default();
form.add_text("field", "first_value");
form.add_text("field", "second_value");
let response = send_form(&srv, form, "/replace").await;
assert_eq!(response.status(), StatusCode::OK);
let mut form = multipart::Form::default();
form.add_text("field", "first_value");
form.add_text("field", "second_value");
let response = send_form(&srv, form, "/ignore").await;
assert_eq!(response.status(), StatusCode::OK);
}
/// Test the Limits.
#[derive(MultipartForm)]
struct TestMemoryUploadLimits {
field: Bytes,
}
#[derive(MultipartForm)]
struct TestFileUploadLimits {
field: TempFile,
}
async fn test_upload_limits_memory(
form: MultipartForm<TestMemoryUploadLimits>,
) -> impl Responder {
assert!(!form.field.data.is_empty());
HttpResponse::Ok().finish()
}
async fn test_upload_limits_file(
form: MultipartForm<TestFileUploadLimits>,
) -> impl Responder {
assert!(form.field.size > 0);
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_memory_limits() {
let srv = actix_test::start(|| {
App::new()
.route("/text", web::post().to(test_upload_limits_memory))
.route("/file", web::post().to(test_upload_limits_file))
.app_data(
MultipartFormConfig::default()
.memory_limit(20)
.total_limit(usize::MAX),
)
});
// Exceeds the 20 byte memory limit
let mut form = multipart::Form::default();
form.add_text("field", "this string is 28 bytes long");
let response = send_form(&srv, form, "/text").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
// Memory limit should not apply when the data is being streamed to disk
let mut form = multipart::Form::default();
form.add_text("field", "this string is 28 bytes long");
let response = send_form(&srv, form, "/file").await;
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_total_limit() {
let srv = actix_test::start(|| {
App::new()
.route("/text", web::post().to(test_upload_limits_memory))
.route("/file", web::post().to(test_upload_limits_file))
.app_data(
MultipartFormConfig::default()
.memory_limit(usize::MAX)
.total_limit(20),
)
});
// Within the 20 byte limit
let mut form = multipart::Form::default();
form.add_text("field", "7 bytes");
let response = send_form(&srv, form, "/text").await;
assert_eq!(response.status(), StatusCode::OK);
// Exceeds the 20 byte overall limit
let mut form = multipart::Form::default();
form.add_text("field", "this string is 28 bytes long");
let response = send_form(&srv, form, "/text").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
// Exceeds the 20 byte overall limit
let mut form = multipart::Form::default();
form.add_text("field", "this string is 28 bytes long");
let response = send_form(&srv, form, "/file").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[derive(MultipartForm)]
struct TestFieldLevelLimits {
#[multipart(limit = "30B")]
field: Vec<Bytes>,
}
async fn test_field_level_limits_route(
form: MultipartForm<TestFieldLevelLimits>,
) -> impl Responder {
assert!(!form.field.is_empty());
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_field_level_limits() {
let srv = actix_test::start(|| {
App::new()
.route("/", web::post().to(test_field_level_limits_route))
.app_data(
MultipartFormConfig::default()
.memory_limit(usize::MAX)
.total_limit(usize::MAX),
)
});
// Within the 30 byte limit
let mut form = multipart::Form::default();
form.add_text("field", "this string is 28 bytes long");
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
// Exceeds the the 30 byte limit
let mut form = multipart::Form::default();
form.add_text("field", "this string is more than 30 bytes long");
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
// Total of values (14 bytes) is within 30 byte limit for "field"
let mut form = multipart::Form::default();
form.add_text("field", "7 bytes");
form.add_text("field", "7 bytes");
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
// Total of values exceeds 30 byte limit for "field"
let mut form = multipart::Form::default();
form.add_text("field", "this string is 28 bytes long");
form.add_text("field", "this string is 28 bytes long");
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
}

View File

@@ -0,0 +1,206 @@
//! Writes a field to a temporary file on disk.
use std::{
io,
path::{Path, PathBuf},
sync::Arc,
};
use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError};
use derive_more::{Display, Error};
use futures_core::future::LocalBoxFuture;
use futures_util::TryStreamExt as _;
use mime::Mime;
use tempfile_dep::NamedTempFile;
use tokio::io::AsyncWriteExt;
use super::FieldErrorHandler;
use crate::{
form::{FieldReader, Limits},
Field, MultipartError,
};
/// Write the field to a temporary file on disk.
#[derive(Debug)]
pub struct TempFile {
/// The temporary file on disk.
pub file: NamedTempFile,
/// The value of the `content-type` header.
pub content_type: Option<Mime>,
/// The `filename` value in the `content-disposition` header.
pub file_name: Option<String>,
/// The size in bytes of the file.
pub size: usize,
}
impl<'t> FieldReader<'t> for TempFile {
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
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_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_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()
.get_filename()
.map(str::to_owned),
size,
})
})
}
}
#[derive(Debug, Display, Error)]
#[non_exhaustive]
pub enum TempFileError {
/// File I/O Error
#[display(fmt = "File I/O error: {}", _0)]
FileIo(std::io::Error),
}
impl ResponseError for TempFileError {
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
}
/// Configuration for the [`TempFile`] field reader.
#[derive(Clone)]
pub struct TempFileConfig {
err_handler: FieldErrorHandler<TempFileError>,
directory: Option<PathBuf>,
}
impl TempFileConfig {
fn create_tempfile(&self) -> io::Result<NamedTempFile> {
if let Some(ref dir) = self.directory {
NamedTempFile::new_in(dir)
} else {
NamedTempFile::new()
}
}
}
impl TempFileConfig {
/// Sets custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(TempFileError, &HttpRequest) -> Error + Send + Sync + 'static,
{
self.err_handler = Some(Arc::new(f));
self
}
/// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
/// back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or(&DEFAULT_CONFIG)
}
fn map_error(
&self,
req: &HttpRequest,
field_name: &str,
err: TempFileError,
) -> MultipartError {
let source = if let Some(ref err_handler) = self.err_handler {
(err_handler)(err, req)
} else {
err.into()
};
MultipartError::Field {
field_name: field_name.to_owned(),
source,
}
}
/// Sets the directory that temp files will be created in.
///
/// The default temporary file location is platform dependent.
pub fn directory(mut self, dir: impl AsRef<Path>) -> Self {
self.directory = Some(dir.as_ref().to_owned());
self
}
}
const DEFAULT_CONFIG: TempFileConfig = TempFileConfig {
err_handler: None,
directory: None,
};
impl Default for TempFileConfig {
fn default() -> Self {
DEFAULT_CONFIG
}
}
#[cfg(test)]
mod tests {
use std::io::{Cursor, Read};
use actix_multipart_rfc7578::client::multipart;
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
use crate::form::{tempfile::TempFile, tests::send_form, MultipartForm};
#[derive(MultipartForm)]
struct FileForm {
file: TempFile,
}
async fn test_file_route(form: MultipartForm<FileForm>) -> impl Responder {
let mut form = form.into_inner();
let mut contents = String::new();
form.file.file.read_to_string(&mut contents).unwrap();
assert_eq!(contents, "Hello, world!");
assert_eq!(form.file.file_name.unwrap(), "testfile.txt");
assert_eq!(form.file.content_type.unwrap(), mime::TEXT_PLAIN);
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_file_upload() {
let srv = actix_test::start(|| App::new().route("/", web::post().to(test_file_route)));
let mut form = multipart::Form::default();
let bytes = Cursor::new("Hello, world!");
form.add_reader_file_with_mime("file", bytes, "testfile.txt", mime::TEXT_PLAIN);
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
}
}

View File

@@ -0,0 +1,196 @@
//! Deserializes a field from plain text.
use std::{str, sync::Arc};
use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError};
use derive_more::{Deref, DerefMut, Display, Error};
use futures_core::future::LocalBoxFuture;
use serde::de::DeserializeOwned;
use super::FieldErrorHandler;
use crate::{
form::{bytes::Bytes, FieldReader, Limits},
Field, MultipartError,
};
/// Deserialize from plain text.
///
/// Internally this uses [`serde_plain`] for deserialization, which supports primitive types
/// including strings, numbers, and simple enums.
#[derive(Debug, Deref, DerefMut)]
pub struct Text<T: DeserializeOwned>(pub T);
impl<T: DeserializeOwned> Text<T> {
/// Unwraps into inner value.
pub fn into_inner(self) -> T {
self.0
}
}
impl<'t, T> FieldReader<'t> for Text<T>
where
T: DeserializeOwned + 'static,
{
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future {
Box::pin(async move {
let config = TextConfig::from_req(req);
let field_name = field.name().to_owned();
if config.validate_content_type {
let valid = if let Some(mime) = field.content_type() {
mime.subtype() == mime::PLAIN || mime.suffix() == Some(mime::PLAIN)
} else {
// https://datatracker.ietf.org/doc/html/rfc7578#section-4.4
// content type defaults to text/plain, so None should be considered valid
true
};
if !valid {
return Err(MultipartError::Field {
field_name,
source: config.map_error(req, TextError::ContentType),
});
}
}
let bytes = Bytes::read_field(req, field, limits).await?;
let text = str::from_utf8(&bytes.data).map_err(|err| MultipartError::Field {
field_name: field_name.clone(),
source: config.map_error(req, TextError::Utf8Error(err)),
})?;
Ok(Text(serde_plain::from_str(text).map_err(|err| {
MultipartError::Field {
field_name,
source: config.map_error(req, TextError::Deserialize(err)),
}
})?))
})
}
}
#[derive(Debug, Display, Error)]
#[non_exhaustive]
pub enum TextError {
/// UTF-8 decoding error.
#[display(fmt = "UTF-8 decoding error: {}", _0)]
Utf8Error(str::Utf8Error),
/// Deserialize error.
#[display(fmt = "Plain text deserialize error: {}", _0)]
Deserialize(serde_plain::Error),
/// Content type error.
#[display(fmt = "Content type error")]
ContentType,
}
impl ResponseError for TextError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// Configuration for the [`Text`] field reader.
#[derive(Clone)]
pub struct TextConfig {
err_handler: FieldErrorHandler<TextError>,
validate_content_type: bool,
}
impl TextConfig {
/// Sets custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(TextError, &HttpRequest) -> Error + Send + Sync + 'static,
{
self.err_handler = Some(Arc::new(f));
self
}
/// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
/// back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or(&DEFAULT_CONFIG)
}
fn map_error(&self, req: &HttpRequest, err: TextError) -> Error {
if let Some(ref err_handler) = self.err_handler {
(err_handler)(err, req)
} else {
err.into()
}
}
/// Sets whether or not the field must have a valid `Content-Type` header to be parsed.
///
/// Note that an empty `Content-Type` is also accepted, as the multipart specification defines
/// `text/plain` as the default for text fields.
pub fn validate_content_type(mut self, validate_content_type: bool) -> Self {
self.validate_content_type = validate_content_type;
self
}
}
const DEFAULT_CONFIG: TextConfig = TextConfig {
err_handler: None,
validate_content_type: true,
};
impl Default for TextConfig {
fn default() -> Self {
DEFAULT_CONFIG
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use actix_multipart_rfc7578::client::multipart;
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
use crate::form::{
tests::send_form,
text::{Text, TextConfig},
MultipartForm,
};
#[derive(MultipartForm)]
struct TextForm {
number: Text<i32>,
}
async fn test_text_route(form: MultipartForm<TextForm>) -> impl Responder {
assert_eq!(*form.number, 1025);
HttpResponse::Ok().finish()
}
#[actix_rt::test]
async fn test_content_type_validation() {
let srv = actix_test::start(|| {
App::new()
.route("/", web::post().to(test_text_route))
.app_data(TextConfig::default().validate_content_type(true))
});
// Deny because wrong content type
let bytes = Cursor::new("1025");
let mut form = multipart::Form::default();
form.add_reader_file_with_mime("number", 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 bytes = Cursor::new("1025");
let mut form = multipart::Form::default();
form.add_reader_file_with_mime("number", bytes, "", mime::TEXT_PLAIN);
let response = send_form(&srv, form, "/").await;
assert_eq!(response.status(), StatusCode::OK);
}
}

View File

@@ -3,10 +3,19 @@
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow(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))]
// This allows us to use the actix_multipart_derive within this crate's tests
#[cfg(test)]
extern crate self as actix_multipart;
mod error;
mod extractor;
mod server;
pub mod form;
pub use self::error::MultipartError;
pub use self::server::{Field, Multipart};

View File

@@ -270,7 +270,9 @@ impl InnerMultipart {
match field.borrow_mut().poll(safety) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(_))) => continue,
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
Poll::Ready(Some(Err(err))) => {
return Poll::Ready(Some(Err(err)))
}
Poll::Ready(None) => true,
}
}
@@ -658,7 +660,7 @@ impl InnerField {
match res {
Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(bytes))) => return Poll::Ready(Some(Ok(bytes))),
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
Poll::Ready(None) => self.eof = true,
}
}
@@ -673,7 +675,7 @@ impl InnerField {
}
Poll::Ready(None)
}
Err(e) => Poll::Ready(Some(Err(e))),
Err(err) => Poll::Ready(Some(Err(err))),
}
} else {
Poll::Pending
@@ -794,7 +796,7 @@ impl PayloadBuffer {
loop {
match Pin::new(&mut self.stream).poll_next(cx) {
Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data),
Poll::Ready(Some(Err(e))) => return Err(e),
Poll::Ready(Some(Err(err))) => return Err(err),
Poll::Ready(None) => {
self.eof = true;
return Ok(());
@@ -860,19 +862,22 @@ impl PayloadBuffer {
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use actix_http::h1::Payload;
use actix_web::http::header::{DispositionParam, DispositionType};
use actix_web::rt;
use actix_web::test::TestRequest;
use actix_web::FromRequest;
use actix_http::h1;
use actix_web::{
http::header::{DispositionParam, DispositionType},
rt,
test::TestRequest,
FromRequest,
};
use bytes::Bytes;
use futures_util::{future::lazy, StreamExt as _};
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use super::*;
#[actix_rt::test]
async fn test_boundary() {
let headers = HeaderMap::new();
@@ -1119,7 +1124,7 @@ mod tests {
#[actix_rt::test]
async fn test_basic() {
let (_, payload) = Payload::create(false);
let (_, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(payload.buf.len(), 0);
@@ -1129,7 +1134,7 @@ mod tests {
#[actix_rt::test]
async fn test_eof() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_max(4).unwrap());
@@ -1145,7 +1150,7 @@ mod tests {
#[actix_rt::test]
async fn test_err() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_max(1).unwrap());
sender.set_error(PayloadError::Incomplete(None));
@@ -1154,7 +1159,7 @@ mod tests {
#[actix_rt::test]
async fn test_readmax() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
sender.feed_data(Bytes::from("line1"));
@@ -1171,7 +1176,7 @@ mod tests {
#[actix_rt::test]
async fn test_readexactly() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_exact(2));
@@ -1189,7 +1194,7 @@ mod tests {
#[actix_rt::test]
async fn test_readuntil() {
let (mut sender, payload) = Payload::create(false);
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_until(b"ne").unwrap());
@@ -1230,7 +1235,7 @@ mod tests {
#[actix_rt::test]
async fn test_multipart_payload_consumption() {
// with sample payload and HttpRequest with no headers
let (_, inner_payload) = Payload::create(false);
let (_, inner_payload) = h1::Payload::create(false);
let mut payload = actix_web::dev::Payload::from(inner_payload);
let req = TestRequest::default().to_http_request();

View File

@@ -1,17 +1,18 @@
# Changes
## Unreleased - 2022-xx-xx
## Unreleased - 2023-xx-xx
## 0.5.1 - 2022-09-19
- Correct typo in error string for `i32` deserialization. [#2876]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
[#2876]: https://github.com/actix/actix-web/pull/2876
## 0.5.0 - 2022-02-22
### Added
- Add `Path::as_str`. [#2590]
- Add `ResourceDef::set_name`. [#373][net#373]
- Add `RouterBuilder::push`. [#2612]
@@ -23,6 +24,7 @@
- Support multi-pattern prefixes and joins. [#2356]
### Changed
- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612]
- Deprecate `Path::path`. [#2590]
- Disallow prefix routes with tail segments. [#379][net#379]
@@ -47,6 +49,7 @@
- Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373]
### Fixed
- Fix `ResourceDef`'s `PartialEq` implementation. [#373][net#373]
- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368]
- Improve malformed path error message. [#384][net#384]
@@ -55,6 +58,7 @@
- Static patterns in multi-patterns are no longer interpreted as regex. [#366][net#366]
### Removed
- `ResourceDef::name_mut`. [#373][net#373]
- Unused `ResourceInfo`. [#2612]
@@ -77,11 +81,11 @@
[net#380]: https://github.com/actix/actix-net/pull/380
[net#384]: https://github.com/actix/actix-net/pull/384
<details>
<summary>0.5.0 Pre-Releases</summary>
## 0.5.0-rc.3 - 2022-01-31
- Remove unused `ResourceInfo`. [#2612]
- Add `RouterBuilder::push`. [#2612]
- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612]
@@ -92,33 +96,33 @@
[#2612]: https://github.com/actix/actix-web/pull/2612
[#2613]: https://github.com/actix/actix-web/pull/2613
## 0.5.0-rc.2 - 2022-01-21
- Add `Path::as_str`. [#2590]
- Deprecate `Path::path`. [#2590]
[#2590]: https://github.com/actix/actix-web/pull/2590
## 0.5.0-rc.1 - 2022-01-14
- `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568]
- `Resource` is now implemented for `&mut Path<_>` and `RefMut<Path<_>>`. [#2568]
[#2568]: https://github.com/actix/actix-web/pull/2568
## 0.5.0-beta.4 - 2022-01-04
- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566]
- Minimum supported Rust version (MSRV) is now 1.54.
[#2566]: https://github.com/actix/actix-net/pull/2566
## 0.5.0-beta.3 - 2021-12-17
- Minimum supported Rust version (MSRV) is now 1.52.
## 0.5.0-beta.2 - 2021-09-09
- Introduce `ResourceDef::join`. [#380][net#380]
- Disallow prefix routes with tail segments. [#379][net#379]
- Enforce path separators on dynamic prefixes. [#378][net#378]
@@ -137,8 +141,8 @@
[#2355]: https://github.com/actix/actix-web/pull/2355
[#2356]: https://github.com/actix/actix-web/pull/2356
## 0.5.0-beta.1 - 2021-07-20
- Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366][net#366]
- Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373]
- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368]
@@ -167,8 +171,8 @@
</details>
## 0.4.0 - 2021-06-06
- When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357][net#357]
- Path tail patterns now match new lines (`\n`) in request URL. [#360][net#360]
- Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359][net#359]
@@ -179,70 +183,70 @@
[net#359]: https://github.com/actix/actix-net/pull/359
[net#360]: https://github.com/actix/actix-net/pull/360
## 0.3.0 - 2019-12-31
- Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0
## 0.2.7 - 2021-02-06
- Add `Router::recognize_checked` [#247][net#247]
[net#247]: https://github.com/actix/actix-net/pull/247
## 0.2.6 - 2021-01-09
- Use `bytestring` version range compatible with Bytes v1.0. [#246][net#246]
[net#246]: https://github.com/actix/actix-net/pull/246
## 0.2.5 - 2020-09-20
- Fix `from_hex()` method
## 0.2.4 - 2019-12-31
- Add `ResourceDef::resource_path_named()` path generation method
## 0.2.3 - 2019-12-25
- Add impl `IntoPattern` for `&String`
## 0.2.2 - 2019-12-25
- Use `IntoPattern` for `RouterBuilder::path()`
## 0.2.1 - 2019-12-25
- Add `IntoPattern` trait
- Add multi-pattern resources
## 0.2.0 - 2019-12-07
- Update http to 0.2
- Update regex to 1.3
- Use bytestring instead of string
## 0.1.5 - 2019-05-15
- Remove debug prints
## 0.1.4 - 2019-05-15
- Fix checked resource match
## 0.1.3 - 2019-04-22
- Added support for `remainder match` (i.e "/path/{tail}*")
- Added support for `remainder match` (i.e "/path/{tail}\*")
## 0.1.2 - 2019-04-07
- Export `Quoter` type
- Allow to reset `Path` instance
## 0.1.1 - 2019-04-03
- Get dynamic segment by name instead of iterator.
## 0.1.0 - 2019-03-09
- Initial release

View File

@@ -21,14 +21,14 @@ default = ["http"]
[dependencies]
bytestring = ">=0.1.5, <2"
http = { version = "0.2.5", optional = true }
http = { version = "0.2.7", optional = true }
regex = "1.5"
serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
[dev-dependencies]
criterion = { version = "0.4", features = ["html_reports"] }
http = "0.2.5"
http = "0.2.7"
serde = { version = "1", features = ["derive"] }
percent-encoding = "2.1"

View File

@@ -5,6 +5,7 @@
#![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))]
mod de;
mod path;

View File

@@ -1,72 +1,76 @@
# Changes
## Unreleased - 2022-xx-xx
## Unreleased - 2023-xx-xx
## 0.1.1 - 2023-02-26
- Add `TestServerConfig::port()` setter method.
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
## 0.1.0 - 2022-07-24
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 0.1.0-beta.13 - 2022-02-16
- No significant changes since `0.1.0-beta.12`.
## 0.1.0-beta.12 - 2022-01-31
- Rename `TestServerConfig::{client_timeout => client_request_timeout}`. [#2611]
[#2611]: https://github.com/actix/actix-web/pull/2611
## 0.1.0-beta.11 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.
## 0.1.0-beta.10 - 2021-12-27
- No significant changes since `0.1.0-beta.9`.
## 0.1.0-beta.9 - 2021-12-17
- Re-export `actix_http::body::to_bytes`. [#2518]
- Update `actix_web::test` re-exports. [#2518]
[#2518]: https://github.com/actix/actix-web/pull/2518
## 0.1.0-beta.8 - 2021-12-11
- No significant changes since `0.1.0-beta.7`.
## 0.1.0-beta.7 - 2021-11-22
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
[#2408]: https://github.com/actix/actix-web/pull/2408
## 0.1.0-beta.6 - 2021-11-15
- No significant changes from `0.1.0-beta.5`.
## 0.1.0-beta.5 - 2021-10-20
- Updated rustls to v0.20. [#2414]
- Minimum supported Rust version (MSRV) is now 1.52.
[#2414]: https://github.com/actix/actix-web/pull/2414
## 0.1.0-beta.4 - 2021-09-09
- Minimum supported Rust version (MSRV) is now 1.51.
## 0.1.0-beta.3 - 2021-06-20
- No significant changes from `0.1.0-beta.2`.
## 0.1.0-beta.2 - 2021-04-17
- No significant changes from `0.1.0-beta.1`.
## 0.1.0-beta.1 - 2021-04-02
- Move integration testing structs from `actix-web`. [#2112]
[#2112]: https://github.com/actix/actix-web/pull/2112

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-test"
version = "0.1.0"
version = "0.1.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
@@ -45,4 +45,4 @@ serde_json = "1"
serde_urlencoded = "0.7"
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
tokio = { version = "1.18.4", features = ["sync"] }
tokio = { version = "1.24.2", features = ["sync"] }

View File

@@ -28,6 +28,9 @@
#![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))]
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;
@@ -145,7 +148,7 @@ where
// run server in separate orphaned thread
thread::spawn(move || {
rt::System::new().block_on(async move {
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
let tcp = net::TcpListener::bind(("127.0.0.1", cfg.port)).unwrap();
let local_addr = tcp.local_addr().unwrap();
let factory = factory.clone();
let srv_cfg = cfg.clone();
@@ -390,6 +393,7 @@ pub struct TestServerConfig {
tp: HttpVer,
stream: StreamType,
client_request_timeout: Duration,
port: u16,
}
impl Default for TestServerConfig {
@@ -405,6 +409,7 @@ impl TestServerConfig {
tp: HttpVer::Both,
stream: StreamType::Tcp,
client_request_timeout: Duration::from_secs(5),
port: 0,
}
}
@@ -439,6 +444,14 @@ impl TestServerConfig {
self.client_request_timeout = dur;
self
}
/// Sets test server port.
///
/// By default, a random free port is determined by the OS.
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
}
/// A basic HTTP server controller that simplifies the process of writing integration tests for

View File

@@ -1,70 +1,73 @@
# Changes
## Unreleased - 2022-xx-xx
## Unreleased - 2023-xx-xx
## 4.2.0 - 2023-01-21
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 4.1.0 - 2022-03-02
- Add support for `actix` version `0.13`. [#2675]
[#2675]: https://github.com/actix/actix-web/pull/2675
## 4.0.0 - 2022-02-25
- No significant changes since `4.0.0-beta.12`.
## 4.0.0-beta.12 - 2022-02-16
- No significant changes since `4.0.0-beta.11`.
## 4.0.0-beta.11 - 2022-01-31
- No significant changes since `4.0.0-beta.10`.
## 4.0.0-beta.10 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.
## 4.0.0-beta.9 - 2021-12-27
- No significant changes since `4.0.0-beta.8`.
## 4.0.0-beta.8 - 2021-12-11
- Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920]
- Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920]
- Minimum supported Rust version (MSRV) is now 1.52.
[#1920]: https://github.com/actix/actix-web/pull/1920
## 4.0.0-beta.7 - 2021-09-09
- Minimum supported Rust version (MSRV) is now 1.51.
## 4.0.0-beta.6 - 2021-06-26
- Update `actix` to `0.12`. [#2277]
[#2277]: https://github.com/actix/actix-web/pull/2277
## 4.0.0-beta.5 - 2021-06-17
- No notable changes.
- No notable changes.
## 4.0.0-beta.4 - 2021-04-02
- No notable changes.
- No notable changes.
## 4.0.0-beta.3 - 2021-03-09
- No notable changes.
- No notable changes.
## 4.0.0-beta.2 - 2021-02-10
- No notable changes.
## 4.0.0-beta.1 - 2021-01-07
- Update `pin-project` to `1.0`.
- Update `bytes` to `1.0`. [#1813]
- `WebsocketContext::text` now takes an `Into<bytestring::ByteString>`. [#1864]
@@ -72,21 +75,21 @@
[#1813]: https://github.com/actix/actix-web/pull/1813
[#1864]: https://github.com/actix/actix-web/pull/1864
## 3.0.0 - 2020-09-11
- No significant changes from `3.0.0-beta.2`.
## 3.0.0-beta.2 - 2020-09-10
- Update `actix-*` dependencies to latest versions.
## [3.0.0-beta.1] - 2020-xx-xx
- Update `actix-web` & `actix-http` dependencies to beta.1
- Bump minimum supported Rust version to 1.40
## [3.0.0-alpha.1] - 2020-05-08
- Update the actix-web dependency to 3.0.0-alpha.1
- Update the actix dependency to 0.10.0-alpha.2
- Update the actix-http dependency to 2.0.0-alpha.3
@@ -109,8 +112,7 @@
## [1.0.2] - 2019-07-20
- Add `ws::start_with_addr()`, returning the address of the created actor, along
with the `HttpResponse`.
- Add `ws::start_with_addr()`, returning the address of the created actor, along with the `HttpResponse`.
- Add support for specifying protocols on websocket handshake #835

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-web-actors"
version = "4.1.0"
version = "4.2.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"]
@@ -23,7 +23,7 @@ bytes = "1"
bytestring = "1"
futures-core = { version = "0.3.17", default-features = false }
pin-project-lite = "0.2"
tokio = { version = "1.18.4", features = ["sync"] }
tokio = { version = "1.24.2", features = ["sync"] }
tokio-util = { version = "0.7", features = ["codec"] }
[dev-dependencies]

View File

@@ -3,15 +3,15 @@
> Actix actors support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.1.0)](https://docs.rs/actix-web-actors/4.1.0)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.2.0)](https://docs.rs/actix-web-actors/4.2.0)
![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-web-actors/4.1.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.1.0)
[![dependency status](https://deps.rs/crate/actix-web-actors/4.2.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.2.0)
[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-actors)
- Minimum Supported Rust Version (MSRV): 1.54
- Minimum Supported Rust Version (MSRV): 1.59

View File

@@ -58,6 +58,9 @@
#![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))]
mod context;
pub mod ws;

View File

@@ -1,44 +1,50 @@
# Changes
## Unreleased - 2022-xx-xx
## Unreleased - 2023-xx-xx
## 4.2.0 - 2023-02-26
- Add support for custom methods with the `#[route]` macro. [#2969]
[#2969]: https://github.com/actix/actix-web/pull/2969
## 4.1.0 - 2022-09-11
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
[#2718]: https://github.com/actix/actix-web/pull/2718
## 4.0.1 - 2022-06-11
- Fix support for guard paths in route handler macros. [#2771]
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
[#2771]: https://github.com/actix/actix-web/pull/2771
## 4.0.0 - 2022-02-24
- Version aligned with `actix-web` and will remain in sync going forward.
- No significant changes since `0.5.0`.
## 0.5.0 - 2022-02-24
- No significant changes since `0.5.0-rc.2`.
## 0.5.0-rc.2 - 2022-02-01
- No significant changes since `0.5.0-rc.1`.
## 0.5.0-rc.1 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.
## 0.5.0-beta.6 - 2021-12-11
- No significant changes since `0.5.0-beta.5`.
## 0.5.0-beta.5 - 2021-10-20
- Improve error recovery potential when macro input is invalid. [#2410]
- Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409]
- Minimum supported Rust version (MSRV) is now 1.52.
@@ -46,90 +52,90 @@
[#2410]: https://github.com/actix/actix-web/pull/2410
[#2409]: https://github.com/actix/actix-web/pull/2409
## 0.5.0-beta.4 - 2021-09-09
- In routing macros, paths are now validated at compile time. [#2350]
- Minimum supported Rust version (MSRV) is now 1.51.
[#2350]: https://github.com/actix/actix-web/pull/2350
## 0.5.0-beta.3 - 2021-06-17
- No notable changes.
## 0.5.0-beta.2 - 2021-03-09
- Preserve doc comments when using route macros. [#2022]
- Add `name` attribute to `route` macro. [#1934]
[#2022]: https://github.com/actix/actix-web/pull/2022
[#1934]: https://github.com/actix/actix-web/pull/1934
## 0.5.0-beta.1 - 2021-02-10
- Use new call signature for `System::new`.
## 0.4.0 - 2020-09-20
- Added compile success and failure testing. [#1677]
- Add `route` macro for supporting multiple HTTP methods guards. [#1674]
[#1677]: https://github.com/actix/actix-web/pull/1677
[#1674]: https://github.com/actix/actix-web/pull/1674
## 0.3.0 - 2020-09-11
- No significant changes from `0.3.0-beta.1`.
## 0.3.0-beta.1 - 2020-07-14
- Add main entry-point macro that uses re-exported runtime. [#1559]
[#1559]: https://github.com/actix/actix-web/pull/1559
## 0.2.2 - 2020-05-23
- Add resource middleware on actix-web-codegen [#1467]
[#1467]: https://github.com/actix/actix-web/pull/1467
## 0.2.1 - 2020-02-25
- Add `#[allow(missing_docs)]` attribute to generated structs [#1368]
- Allow the handler function to be named as `config` [#1290]
[#1368]: https://github.com/actix/actix-web/issues/1368
[#1290]: https://github.com/actix/actix-web/issues/1290
## 0.2.0 - 2019-12-13
- Generate code for actix-web 2.0
## 0.1.3 - 2019-10-14
- Bump up `syn` & `quote` to 1.0
- Provide better error message
## 0.1.2 - 2019-06-04
- Add macros for head, options, trace, connect and patch http methods
## 0.1.1 - 2019-06-01
- Add syn "extra-traits" feature
## 0.1.0 - 2019-05-18
- Release
## 0.1.0-beta.1 - 2019-04-20
- Gen code for actix-web 1.0.0-beta.1
## 0.1.0-alpha.6 - 2019-04-14
- Gen code for actix-web 1.0.0-alpha.6
## 0.1.0-alpha.1 - 2019-03-28
- Initial impl

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-web-codegen"
version = "4.1.0"
version = "4.2.0"
description = "Routing and runtime macros for Actix Web"
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"

View File

@@ -3,18 +3,18 @@
> Routing and runtime macros for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.1.0)](https://docs.rs/actix-web-codegen/4.1.0)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.2.0)](https://docs.rs/actix-web-codegen/4.2.0)
![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/4.1.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.1.0)
[![dependency status](https://deps.rs/crate/actix-web-codegen/4.2.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.2.0)
[![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-codegen)
- Minimum Supported Rust Version (MSRV): 1.54
- Minimum Supported Rust Version (MSRV): 1.59
## Compile Testing

View File

@@ -75,6 +75,9 @@
#![recursion_limit = "512"]
#![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))]
use proc_macro::TokenStream;
use quote::quote;
@@ -105,7 +108,7 @@ mod route;
/// ```
/// # use actix_web::HttpResponse;
/// # use actix_web_codegen::route;
/// #[route("/test", method = "GET", method = "HEAD")]
/// #[route("/test", method = "GET", method = "HEAD", method = "CUSTOM")]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }

View File

@@ -6,11 +6,11 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
macro_rules! method_type {
macro_rules! standard_method_type {
(
$($variant:ident, $upper:ident, $lower:ident,)+
) => {
#[derive(Debug, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MethodType {
$(
$variant,
@@ -27,7 +27,7 @@ macro_rules! method_type {
fn parse(method: &str) -> Result<Self, String> {
match method {
$(stringify!($upper) => Ok(Self::$variant),)+
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
_ => Err(format!("HTTP method must be uppercase: `{}`", method)),
}
}
@@ -41,7 +41,7 @@ macro_rules! method_type {
};
}
method_type! {
standard_method_type! {
Get, GET, get,
Post, POST, post,
Put, PUT, put,
@@ -53,13 +53,6 @@ method_type! {
Patch, PATCH, patch,
}
impl ToTokens for MethodType {
fn to_tokens(&self, stream: &mut TokenStream2) {
let ident = Ident::new(self.as_str(), Span::call_site());
stream.append(ident);
}
}
impl TryFrom<&syn::LitStr> for MethodType {
type Error = syn::Error;
@@ -69,12 +62,123 @@ impl TryFrom<&syn::LitStr> for MethodType {
}
}
impl ToTokens for MethodType {
fn to_tokens(&self, stream: &mut TokenStream2) {
let ident = Ident::new(self.as_str(), Span::call_site());
stream.append(ident);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum MethodTypeExt {
Standard(MethodType),
Custom(LitStr),
}
impl MethodTypeExt {
/// Returns a single method guard token stream.
fn to_tokens_single_guard(&self) -> TokenStream2 {
match self {
MethodTypeExt::Standard(method) => {
quote! {
.guard(::actix_web::guard::#method())
}
}
MethodTypeExt::Custom(lit) => {
quote! {
.guard(::actix_web::guard::Method(
::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
))
}
}
}
}
/// Returns a multi-method guard chain token stream.
fn to_tokens_multi_guard(&self, or_chain: Vec<impl ToTokens>) -> TokenStream2 {
debug_assert!(
!or_chain.is_empty(),
"empty or_chain passed to multi-guard constructor"
);
match self {
MethodTypeExt::Standard(method) => {
quote! {
.guard(
::actix_web::guard::Any(::actix_web::guard::#method())
#(#or_chain)*
)
}
}
MethodTypeExt::Custom(lit) => {
quote! {
.guard(
::actix_web::guard::Any(
::actix_web::guard::Method(
::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
)
)
#(#or_chain)*
)
}
}
}
}
/// Returns a token stream containing the `.or` chain to be passed in to
/// [`MethodTypeExt::to_tokens_multi_guard()`].
fn to_tokens_multi_guard_or_chain(&self) -> TokenStream2 {
match self {
MethodTypeExt::Standard(method_type) => {
quote! {
.or(::actix_web::guard::#method_type())
}
}
MethodTypeExt::Custom(lit) => {
quote! {
.or(
::actix_web::guard::Method(
::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
)
)
}
}
}
}
}
impl ToTokens for MethodTypeExt {
fn to_tokens(&self, stream: &mut TokenStream2) {
match self {
MethodTypeExt::Custom(lit_str) => {
let ident = Ident::new(lit_str.value().as_str(), Span::call_site());
stream.append(ident);
}
MethodTypeExt::Standard(method) => method.to_tokens(stream),
}
}
}
impl TryFrom<&syn::LitStr> for MethodTypeExt {
type Error = syn::Error;
fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
match MethodType::try_from(value) {
Ok(method) => Ok(MethodTypeExt::Standard(method)),
Err(_) if value.value().chars().all(|c| c.is_ascii_uppercase()) => {
Ok(MethodTypeExt::Custom(value.clone()))
}
Err(err) => Err(err),
}
}
}
struct Args {
path: syn::LitStr,
resource_name: Option<syn::LitStr>,
guards: Vec<Path>,
wrappers: Vec<syn::Type>,
methods: HashSet<MethodType>,
methods: HashSet<MethodTypeExt>,
}
impl Args {
@@ -99,7 +203,7 @@ impl Args {
let is_route_macro = method.is_none();
if let Some(method) = method {
methods.insert(method);
methods.insert(MethodTypeExt::Standard(method));
}
for arg in args {
@@ -116,6 +220,7 @@ impl Args {
));
}
},
NestedMeta::Meta(syn::Meta::NameValue(nv)) => {
if nv.path.is_ident("name") {
if let syn::Lit::Str(lit) = nv.lit {
@@ -151,8 +256,7 @@ impl Args {
"HTTP method forbidden here. To handle multiple methods, use `route` instead",
));
} else if let syn::Lit::Str(ref lit) = nv.lit {
let method = MethodType::try_from(lit)?;
if !methods.insert(method) {
if !methods.insert(MethodTypeExt::try_from(lit)?) {
return Err(syn::Error::new_spanned(
&nv.lit,
format!(
@@ -174,11 +278,13 @@ impl Args {
));
}
}
arg => {
return Err(syn::Error::new_spanned(arg, "Unknown attribute."));
}
}
}
Ok(Args {
path: path.unwrap(),
resource_name,
@@ -299,22 +405,19 @@ impl ToTokens for Route {
.map_or_else(|| name.to_string(), LitStr::value);
let method_guards = {
let mut others = methods.iter();
debug_assert!(!methods.is_empty(), "Args::methods should not be empty");
// unwrapping since length is checked to be at least one
let mut others = methods.iter();
let first = others.next().unwrap();
if methods.len() > 1 {
quote! {
.guard(
::actix_web::guard::Any(::actix_web::guard::#first())
#(.or(::actix_web::guard::#others()))*
)
}
let other_method_guards = others
.map(|method_ext| method_ext.to_tokens_multi_guard_or_chain())
.collect();
first.to_tokens_multi_guard(other_method_guards)
} else {
quote! {
.guard(::actix_web::guard::#first())
}
first.to_tokens_single_guard()
}
};
@@ -325,7 +428,6 @@ impl ToTokens for Route {
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.to(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
}
})

View File

@@ -86,7 +86,18 @@ async fn get_param_test(_: web::Path<String>) -> impl Responder {
HttpResponse::Ok()
}
#[route("/multi", method = "GET", method = "POST", method = "HEAD")]
#[route("/hello", method = "HELLO")]
async fn custom_route_test() -> impl Responder {
HttpResponse::Ok()
}
#[route(
"/multi",
method = "GET",
method = "POST",
method = "HEAD",
method = "HELLO"
)]
async fn route_test() -> impl Responder {
HttpResponse::Ok()
}

View File

@@ -9,9 +9,11 @@ fn compile_macros() {
t.pass("tests/trybuild/route-ok.rs");
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
t.pass("tests/trybuild/route-custom-method.rs");
t.compile_fail("tests/trybuild/route-custom-lowercase.rs");
t.pass("tests/trybuild/routes-ok.rs");
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");

View File

@@ -1,6 +1,8 @@
use actix_web_codegen::*;
use actix_web::http::Method;
use std::str::FromStr;
#[route("/", method="UNEXPECTED")]
#[route("/", method = "hello")]
async fn index() -> String {
"Hello World!".to_owned()
}
@@ -11,7 +13,7 @@ async fn main() {
let srv = actix_test::start(|| App::new().service(index));
let request = srv.get("/");
let request = srv.request(Method::from_str("hello").unwrap(), srv.url("/"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@@ -0,0 +1,19 @@
error: HTTP method must be uppercase: `hello`
--> tests/trybuild/route-custom-lowercase.rs:5:23
|
5 | #[route("/", method = "hello")]
| ^^^^^^^
error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied
--> tests/trybuild/route-custom-lowercase.rs:14:55
|
14 | let srv = actix_test::start(|| App::new().service(index));
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future<Output = String> {index}`
| |
| required by a bound introduced by this call
|
note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs
|
| F: HttpServiceFactory + 'static,
| ^^^^^^^^^^^^^^^^^^ required by this bound in `App::<T>::service`

View File

@@ -0,0 +1,37 @@
use std::str::FromStr;
use actix_web::http::Method;
use actix_web_codegen::route;
#[route("/single", method = "CUSTOM")]
async fn index() -> String {
"Hello Single!".to_owned()
}
#[route("/multi", method = "GET", method = "CUSTOM")]
async fn custom() -> String {
"Hello Multi!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::App;
let srv = actix_test::start(|| App::new().service(index).service(custom));
let request = srv.request(Method::GET, srv.url("/"));
let response = request.send().await.unwrap();
assert!(response.status().is_client_error());
let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/single"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(Method::GET, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@@ -1,19 +0,0 @@
error: Unexpected HTTP method: `UNEXPECTED`
--> tests/trybuild/route-unexpected-method-fail.rs:3:21
|
3 | #[route("/", method="UNEXPECTED")]
| ^^^^^^^^^^^^
error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied
--> tests/trybuild/route-unexpected-method-fail.rs:12:55
|
12 | let srv = actix_test::start(|| App::new().service(index));
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future<Output = String> {index}`
| |
| required by a bound introduced by this call
|
note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs
|
| F: HttpServiceFactory + 'static,
| ^^^^^^^^^^^^^^^^^^ required by this bound in `App::<T>::service`

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-web"
version = "4.2.1"
version = "4.3.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
@@ -70,9 +70,9 @@ actix-tls = { version = "3", default-features = false, optional = true }
actix-http = { version = "3.3", features = ["http2", "ws"] }
actix-router = "0.5"
actix-web-codegen = { version = "4.1", optional = true }
actix-web-codegen = { version = "4.2", optional = true }
ahash = "0.7"
ahash = "0.8"
bytes = "1"
bytestring = "1"
cfg-if = "1"
@@ -81,7 +81,6 @@ derive_more = "0.99.8"
encoding_rs = "0.8"
futures-core = { version = "0.3.17", default-features = false }
futures-util = { version = "0.3.17", default-features = false }
http = "0.2.8"
itoa = "1"
language-tags = "0.3"
log = "0.4"
@@ -93,7 +92,7 @@ serde = "1.0"
serde_json = "1.0"
serde_urlencoded = "0.7"
smallvec = "1.6.1"
socket2 = "0.4.0"
socket2 = "0.4"
time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1"
@@ -103,7 +102,7 @@ actix-test = { version = "0.1", features = ["openssl", "rustls"] }
awc = { version = "3", features = ["openssl"] }
brotli = "3.3.3"
const-str = "0.4"
const-str = "0.3"
criterion = { version = "0.4", features = ["html_reports"] }
env_logger = "0.9"
flate2 = "1.0.13"
@@ -115,7 +114,7 @@ serde = { version = "1.0", features = ["derive"] }
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.18.4", features = ["rt-multi-thread", "macros"] }
tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] }
zstd = "0.12"
[[test]]

View File

@@ -1,7 +1,6 @@
# 0.7.15
- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
your routes, you should use `%20`.
- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in your routes, you should use `%20`.
instead of
@@ -29,13 +28,11 @@ fn main() {
# 0.7.4
- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
even for handler with one parameter.
- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple even for handler with one parameter.
# 0.7
- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
use `HttpMessage::payload()` method.
- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload use `HttpMessage::payload()` method.
instead of
@@ -60,8 +57,7 @@ fn index(req: HttpRequest) -> impl Responder {
}
```
- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
trait uses `&HttpRequest` instead of `&mut HttpRequest`.
- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) trait uses `&HttpRequest` instead of `&mut HttpRequest`.
- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
@@ -81,14 +77,11 @@ fn index((query, json): (Query<..>, Json<MyStruct)) -> impl Responder {}
- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
- Removed deprecated `HttpServer::threads()`, use
[HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
- Removed deprecated `HttpServer::threads()`, use [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
- Renamed `client::ClientConnectorError::Connector` to
`client::ClientConnectorError::Resolver`
- Renamed `client::ClientConnectorError::Connector` to `client::ClientConnectorError::Resolver`
- `Route::with()` does not return `ExtractorConfig`, to configure
extractor use `Route::with_config()`
- `Route::with()` does not return `ExtractorConfig`, to configure extractor use `Route::with_config()`
instead of
@@ -116,23 +109,19 @@ fn main() {
}
```
- `Route::with_async()` does not return `ExtractorConfig`, to configure
extractor use `Route::with_async_config()`
- `Route::with_async()` does not return `ExtractorConfig`, to configure extractor use `Route::with_async_config()`
# 0.6
- `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
- `ws::Message::Close` now includes optional close reason.
`ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
- `ws::Message::Close` now includes optional close reason. `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
- `HttpServer::threads()` renamed to `HttpServer::workers()`.
- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
- `HttpRequest::extensions()` returns read only reference to the request's Extension
`HttpRequest::extensions_mut()` returns mutable reference.
- `HttpRequest::extensions()` returns read only reference to the request's Extension `HttpRequest::extensions_mut()` returns mutable reference.
- Instead of
@@ -146,8 +135,7 @@ fn main() {
- `FromRequest::Result` has to implement `Into<Reply<Self>>`
- [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
is generic over `S`
- [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) is generic over `S`
- Use `Query` extractor instead of HttpRequest::query()`.
@@ -163,23 +151,19 @@ or
let q = Query::<HashMap<String, String>>::extract(req);
```
- Websocket operations are implemented as `WsWriter` trait.
you need to use `use actix_web::ws::WsWriter`
- Websocket operations are implemented as `WsWriter` trait. you need to use `use actix_web::ws::WsWriter`
# 0.5
- `HttpResponseBuilder::body()`, `.finish()`, `.json()`
methods return `HttpResponse` instead of `Result<HttpResponse>`
- `HttpResponseBuilder::body()`, `.finish()`, `.json()` methods return `HttpResponse` instead of `Result<HttpResponse>`
- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
moved to `actix_web::http` module
- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` moved to `actix_web::http` module
- `actix_web::header` moved to `actix_web::http::header`
- `NormalizePath` moved to `actix_web::http` module
- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
shortcut for `actix_web::server::HttpServer::new()`
- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, shortcut for `actix_web::server::HttpServer::new()`
- `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
@@ -187,11 +171,9 @@ let q = Query::<HashMap<String, String>>::extract(req);
- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
functions should be used instead
- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` functions should be used instead
- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
instead of `Result<_, http::Error>`
- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` instead of `Result<_, http::Error>`
- `Application` renamed to a `App`

View File

@@ -88,8 +88,7 @@
)
```
- Resource registration. 1.0 version uses generalized resource
registration via `.service()` method.
- Resource registration. 1.0 version uses generalized resource registration via `.service()` method.
instead of
@@ -97,9 +96,7 @@
App.new().resource("/welcome", |r| r.f(welcome))
```
use App's or Scope's `.service()` method. `.service()` method accepts
object that implements `HttpServiceFactory` trait. By default
actix-web provides `Resource` and `Scope` services.
use App's or Scope's `.service()` method. `.service()` method accepts object that implements `HttpServiceFactory` trait. By default actix-web provides `Resource` and `Scope` services.
```rust
App.new().service(
@@ -164,9 +161,7 @@
}
```
- `.f()`, `.a()` and `.h()` handler registration methods have been removed.
Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
must use extractors.
- `.f()`, `.a()` and `.h()` handler registration methods have been removed. Use `.to()` for handlers and `.to_async()` for async handlers. Handler function must use extractors.
instead of
@@ -210,9 +205,7 @@
}
```
- `State` is now `Data`. You register Data during the App initialization process
and then access it from handlers either using a Data extractor or using
HttpRequest's api.
- `State` is now `Data`. You register Data during the App initialization process and then access it from handlers either using a Data extractor or using HttpRequest's api.
instead of
@@ -277,8 +270,7 @@
.route("/index.html", web::get().to(index));
```
- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
instead of
@@ -317,8 +309,7 @@
use `use actix_multipart::Multipart`
- Response compression is not enabled by default.
To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
- Response compression is not enabled by default. To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
- Session middleware moved to actix-session crate

View File

@@ -1,21 +1,16 @@
# Migrating to 2.0.0
- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
`.await` on `run` method result, in that case it awaits server exit.
- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to `.await` on `run` method result, in that case it awaits server exit.
- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`.
Stored data is available via `HttpRequest::app_data()` method at runtime.
- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`. Stored data is available via `HttpRequest::app_data()` method at runtime.
- Extractor configuration must be registered with `App::app_data()` instead of `App::data()`
- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()`
replace `fn` with `async fn` to convert sync handler to async
- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()` replace `fn` with `async fn` to convert sync handler to async
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start
test server use `test::start()` or `test_start_with_config()` methods
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start test server use `test::start()` or `test_start_with_config()` methods
- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders
http response.
- `ResponseError` trait has been refactored. `ResponseError::error_response()` renders http response.
- Feature `rust-tls` renamed to `rustls`

View File

@@ -1,31 +1,23 @@
# Migrating to 3.0.0
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
- Cookie handling has been offloaded to the `cookie` crate:
- `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
- Some types now require lifetime parameters.
- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
any `actix-web` method previously expecting a time v0.1 input.
- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects any `actix-web` method previously expecting a time v0.1 input.
- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
result in `SameSite=None` being sent with the response Set-Cookie header.
To create a cookie without a SameSite attribute, remove any calls setting same_site.
- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now result in `SameSite=None` being sent with the response Set-Cookie header. To create a cookie without a SameSite attribute, remove any calls setting same_site.
- actix-http support for Actors messages was moved to actix-http crate and is enabled
with feature `actors`
- actix-http support for Actors messages was moved to actix-http crate and is enabled with feature `actors`
- content_length function is removed from actix-http.
You can set Content-Length by normally setting the response body or calling no_chunking function.
- content_length function is removed from actix-http. You can set Content-Length by normally setting the response body or calling no_chunking function.
- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
`u64` instead of a `usize`.
- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`.
- Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use
destructuring or `.into_inner()`. For example:
- Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use destructuring or `.into_inner()`. For example:
```rust
// Previously:
@@ -44,9 +36,7 @@
}
```
- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`,
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.

View File

@@ -31,7 +31,7 @@ Headings marked with :warning: are **breaking behavioral changes**. They will pr
- [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously)
- [`#[actix_web::main]` and `#[tokio::main]`](#actix_webmain-and-tokiomain)
- [`web::block`](#webblock)
-
-
## MSRV

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