Compare commits

...

155 Commits

Author SHA1 Message Date
dependabot[bot] c284426698
build(deps): bump once_cell from 1.21.1 to 1.21.3 (#3606)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.21.1 to 1.21.3.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.21.1...v1.21.3)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 07:57:06 +00:00
dependabot[bot] b75ae11e62
build(deps): bump log from 0.4.26 to 0.4.27 (#3608)
Bumps [log](https://github.com/rust-lang/log) from 0.4.26 to 0.4.27.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.26...0.4.27)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 07:57:04 +00:00
dependabot[bot] 7fddaae878
build(deps): bump taiki-e/install-action from 2.49.33 to 2.49.39 (#3609)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.33 to 2.49.39.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.49.33...v2.49.39)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 07:57:03 +00:00
dependabot[bot] 52264448cd
build(deps): bump socket2 from 0.5.8 to 0.5.9 (#3607)
Bumps [socket2](https://github.com/rust-lang/socket2) from 0.5.8 to 0.5.9.
- [Release notes](https://github.com/rust-lang/socket2/releases)
- [Changelog](https://github.com/rust-lang/socket2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/socket2/commits)

---
updated-dependencies:
- dependency-name: socket2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 07:56:34 +00:00
dependabot[bot] 90c19a835d
build(deps): bump taiki-e/install-action from 2.49.32 to 2.49.33 (#3602)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.32 to 2.49.33.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.49.32...v2.49.33)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 11:52:08 +00:00
dependabot[bot] adf57d2b24
build(deps): bump time from 0.3.40 to 0.3.41 (#3601)
Bumps [time](https://github.com/time-rs/time) from 0.3.40 to 0.3.41.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.40...v0.3.41)

---
updated-dependencies:
- dependency-name: time
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 11:51:57 +00:00
dependabot[bot] fcd10fbb5e
build(deps): bump taiki-e/install-action from 2.49.17 to 2.49.27 (#3600)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.17 to 2.49.27.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.49.17...v2.49.27)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 06:58:14 +00:00
Rob Ede 95b6a81f43
refactor: switch size parsing to bytesize crate 2025-03-21 06:06:50 +00:00
Rob Ede ab18efe0ac
chore: check in lockfile 2025-03-21 05:51:45 +00:00
Rob Ede cede0c6dbb
chore(actix-web): prepare release 4.10.2 2025-03-10 04:52:02 +00:00
Rob Ede 1005b6a12a
chore: fix actix-http ver req 2025-03-10 04:51:37 +00:00
Rob Ede d898e8f739
chore(actix-web): prepare release 4.10.1 2025-03-10 04:38:45 +00:00
Rob Ede 353873fc04
chore: fix derive-more feature selection 2025-03-10 04:38:20 +00:00
Rob Ede 1390e29705
docs: fix lint 2025-03-10 04:23:20 +00:00
Rob Ede c6e7ebd185
refactor: use Payload::from internally 2025-03-10 04:23:03 +00:00
dependabot[bot] e8351cc3aa
build(deps): bump taiki-e/install-action from 2.49.10 to 2.49.17 (#3597)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.10 to 2.49.17.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.49.10...v2.49.17)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 00:58:26 +00:00
Rob Ede f63cf69e6a
docs: remove "copyright" 2025-03-09 19:08:00 +00:00
Rob Ede 92c1e2230d
chore(awc): prepare release 3.6.0 2025-03-09 19:03:27 +00:00
Rob Ede 4bb495aba0
chore(actix-web): prepare release 4.10.0 2025-03-09 19:02:33 +00:00
Rob Ede aa000b429d
chore(actix-http): prepare release 3.10.0 2025-03-09 19:01:37 +00:00
Bernard Assan df0885cf21
Add from_bytes/u8_bytes to dev::Payload (#3595)
* feat: Add from_bytes/u8_bytes to dev::Payload

This allows convinent construction of Payload from bytes which is
useful in middlewares

closes actix/actix-web#3589

Add doc comment and changelog entry

* implement from<bytes/vec> for payload

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-03-09 16:40:00 +00:00
dependabot[bot] 0796f8e796
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.10.1 to 1.11.0 (#3592)
build(deps): bump actions-rust-lang/setup-rust-toolchain

Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.10.1 to 1.11.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.10.1...v1.11.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-09 04:50:17 +00:00
dependabot[bot] a2307fbb86
build(deps): bump codecov/codecov-action from 5.3.1 to 5.4.0 (#3591)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.3.1...v5.4.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-09 04:44:29 +00:00
dependabot[bot] 98ced477f7
build(deps): bump ilammy/setup-nasm from 1.5.1 to 1.5.2 (#3593)
Bumps [ilammy/setup-nasm](https://github.com/ilammy/setup-nasm) from 1.5.1 to 1.5.2.
- [Release notes](https://github.com/ilammy/setup-nasm/releases)
- [Commits](https://github.com/ilammy/setup-nasm/compare/v1.5.1...v1.5.2)

---
updated-dependencies:
- dependency-name: ilammy/setup-nasm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-09 04:42:41 +00:00
dependabot[bot] 98c263b3ee
build(deps): bump taiki-e/install-action from 2.49.0 to 2.49.10 (#3590)
* build(deps): bump taiki-e/install-action from 2.49.0 to 2.49.10

Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.0 to 2.49.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.49.0...v2.49.10)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* ci: fix msrv

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-03-09 04:20:55 +00:00
dependabot[bot] b8bdee0606
build(deps): bump taiki-e/install-action from 2.48.13 to 2.49.0 (#3586)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.48.13 to 2.49.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.48.13...v2.49.0)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 00:59:19 +00:00
dependabot[bot] 85843b9b0f
build(deps): bump taiki-e/install-action from 2.48.4 to 2.48.13 (#3580)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.48.4 to 2.48.13.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.48.4...v2.48.13)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 18:38:45 +00:00
dependabot[bot] 9656383646
build(deps): update derive_more requirement from 1 to 2 (#3571)
* build(deps): update derive_more requirement from 1 to 2

Updates the requirements on [derive_more](https://github.com/JelteF/derive_more) to permit the latest version.
- [Release notes](https://github.com/JelteF/derive_more/releases)
- [Changelog](https://github.com/JelteF/derive_more/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JelteF/derive_more/compare/v1.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: derive_more
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* refactor: simplify derive_more calls

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-02-10 01:27:56 +00:00
dependabot[bot] cee7451915
build(deps): bump taiki-e/install-action from 2.48.1 to 2.48.4 (#3572)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.48.1 to 2.48.4.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.48.1...v2.48.4)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 01:10:23 +00:00
dependabot[bot] eb6f6a1976
build(deps): bump taiki-e/cache-cargo-install-action from 2.1.0 to 2.1.1 (#3573)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/taiki-e/cache-cargo-install-action/releases)
- [Changelog](https://github.com/taiki-e/cache-cargo-install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/cache-cargo-install-action/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 01:09:53 +00:00
Akos Vandra-Meyer 04533a15fa
Add `Extensions::get_or_insert[_with]()` methods (#3561)
* add get_or_insert and get_or_insert_with for Extensions

* add docs

* fix doctest

* docs: update changelog

* chore: simplify get_or_insert

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-02-09 22:04:21 +00:00
Akos Vandra-Meyer a4eaa7f0bb
implement Responder for Result<(), E: Error> (#3560)
* implement Responder for Option<()> and Result<(), E: Error>

* chore: remove Option<()> impl

* chore: fix changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-02-09 03:01:39 +00:00
dependabot[bot] 66e2afe306
build(deps): update rand requirement from 0.8 to 0.9 (#3564)
* build(deps): update rand requirement from 0.8 to 0.9

Updates the requirements on [rand](https://github.com/rust-random/rand) to permit the latest version.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.8.0...0.9.0)

---
updated-dependencies:
- dependency-name: rand
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: fix rand upgrade

* chore: address clippy lint

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-02-09 02:39:22 +00:00
Rob Ede 59961a58a8
chore: fix msrv errors 2025-02-09 02:15:43 +00:00
Rob Ede 33b487e854
chore: address clippy lints 2025-02-09 01:41:07 +00:00
dependabot[bot] 182055bcb5
build(deps): bump codecov/codecov-action from 5.1.2 to 5.3.1 (#3554)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.2 to 5.3.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.1.2...v5.3.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-03 18:59:22 +00:00
dependabot[bot] a36280466c
build(deps): bump taiki-e/cache-cargo-install-action from 2.0.1 to 2.1.0 (#3555)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 2.0.1 to 2.1.0.
- [Release notes](https://github.com/taiki-e/cache-cargo-install-action/releases)
- [Changelog](https://github.com/taiki-e/cache-cargo-install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/cache-cargo-install-action/compare/v2.0.1...v2.1.0)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-03 18:59:09 +00:00
dependabot[bot] 8690f80a08
build(deps): bump taiki-e/install-action from 2.47.18 to 2.48.1 (#3563)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.47.18 to 2.48.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.47.18...v2.48.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-03 16:57:05 +00:00
dependabot[bot] 91e29c0ce4
build(deps): bump taiki-e/install-action from 2.47.11 to 2.47.18 (#3551)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.47.11 to 2.47.18.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.47.11...v2.47.18)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 14:04:49 +00:00
dependabot[bot] b0fe679784
build(deps): bump taiki-e/install-action from 2.47.7 to 2.47.11 (#3550)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 01:47:23 +00:00
dependabot[bot] 0fafb486d4
build(deps): bump taiki-e/install-action from 2.47.0 to 2.47.7 (#3544)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.47.0 to 2.47.7.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.47.0...v2.47.7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-06 08:08:33 +00:00
dependabot[bot] 5aeb0dd950
build(deps): bump taiki-e/install-action from 2.46.20 to 2.47.0 (#3540)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.46.20 to 2.47.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.46.20...v2.47.0)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 01:24:31 +00:00
Joel Wurtz 856480cd90
fix(http2): remove host header when using http2 (#3516)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-12-29 16:55:32 +00:00
dependabot[bot] bb1442e20b
build(deps): update actix-multipart-rfc7578 requirement from 0.10 to 0.11 (#3517)
build(deps): update actix-multipart-rfc7578 requirement

---
updated-dependencies:
- dependency-name: actix-multipart-rfc7578
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-29 16:50:44 +00:00
Rob Ede ac2a3bb124
refactor: replace ahash with foldhash (#3483) 2024-12-29 16:20:00 +00:00
Onè 8200e4ee82
docs: reword sentence (#3530)
* docs: reword sentence 

The sentence was a bit hard to follow. I think it seems more natural now.

* docs: fix whitespace

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-12-29 14:52:29 +00:00
Rob Ede 5b60d81f57
docs: fix changelog 2024-12-29 15:18:40 +00:00
Rob Ede ee6a6ec03e
docs: add tls to awc example 2024-12-29 15:17:18 +00:00
Rob Ede 34327bd221
chore: address clippy warnings 2024-12-29 15:03:43 +00:00
Rob Ede 472dbca64e
ci: remove public-api-diff job 2024-12-29 14:29:37 +00:00
dependabot[bot] d8566da66f
build(deps): bump codecov/codecov-action from 5.0.7 to 5.1.2 (#3536)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.7 to 5.1.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.0.7...v5.1.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 22:54:53 +00:00
dependabot[bot] a908afa56b
build(deps): bump taiki-e/install-action from 2.45.6 to 2.46.20 (#3537)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.45.6 to 2.46.20.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.45.6...v2.46.20)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-24 09:22:53 +00:00
Joel Wurtz 8115c818c1
Fix continuous integration (#3526)
fix(ci): downgrade divan to 0.1.15 on msrv
2024-12-10 16:11:12 +00:00
dependabot[bot] 002c1b5a19
build(deps): bump taiki-e/install-action from 2.44.71 to 2.45.6 (#3509)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.44.71 to 2.45.6.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.44.71...v2.45.6)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 00:43:45 +00:00
dependabot[bot] 836c75064b
build(deps): bump codecov/codecov-action from 5.0.2 to 5.0.7 (#3508)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.2 to 5.0.7.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.0.2...v5.0.7)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 00:42:41 +00:00
dependabot[bot] 2132c95b01
build(deps): bump codecov/codecov-action from 4.6.0 to 5.0.2 (#3504)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.6.0 to 5.0.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.6.0...v5.0.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 11:26:30 +00:00
dependabot[bot] eff2a20c90
build(deps): bump taiki-e/install-action from 2.44.69 to 2.44.71 (#3505)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.44.69 to 2.44.71.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.44.69...v2.44.71)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 11:26:12 +00:00
dependabot[bot] 9d849c19a5
build(deps): bump taiki-e/install-action from 2.44.60 to 2.44.69 (#3502)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.44.60 to 2.44.69.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.44.60...v2.44.69)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-14 08:55:07 +00:00
Rob Ede 6771be20b3
ci: nightly rust versions in variables (#3501) 2024-11-14 08:29:00 +00:00
dependabot[bot] ef977055fc
build(deps): bump taiki-e/install-action from 2.44.43 to 2.44.60 (#3494)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 21:48:34 +00:00
dependabot[bot] 568bffeb58
build(deps): bump taiki-e/install-action from 2.44.34 to 2.44.43 (#3488)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.44.34 to 2.44.43.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.44.34...v2.44.43)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 12:19:31 +00:00
Rob Ede 03c65d93e5
docs: add from_fn examples 2024-10-15 08:35:39 +01:00
Rob Ede ec05381f6f
feat: add CLEAR_SITE_DATA header 2024-10-15 07:01:01 +01:00
dependabot[bot] 4c05c87b11
build(deps): bump taiki-e/install-action from 2.44.24 to 2.44.34 (#3485) 2024-10-14 20:07:05 +02:00
Durairaj Subramaniam 27c07f122b
fix: service macro comments (#3474)
* fix: service macro comments #3472

* test: service macro comments #3472

* fix: add case for empty tuple seperately

* doc: add case for empty tuple seperately

* test: move test_services into lib

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-10-07 22:03:38 +00:00
Luca Iachini 3849cdaa6c
Improve worker_max_blocking_threads documentation (#3477)
* improve worker_max_blocking_threads doc

* docs: tweak doc

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-10-07 21:16:10 +00:00
dependabot[bot] a5c2d0531b
build(deps): update brotli requirement from 6 to 7 (#3482)
* build(deps): update brotli requirement from 6 to 7

Updates the requirements on [brotli](https://github.com/dropbox/rust-brotli) to permit the latest version.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits)

---
updated-dependencies:
- dependency-name: brotli
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: update changelogs

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-10-07 20:40:14 +00:00
Rob Ede 049b49290d
docs: fix long paragraph warning 2024-10-07 21:23:21 +01:00
dependabot[bot] b7a0ff0a3a
build(deps): bump codecov/codecov-action from 4.5.0 to 4.6.0 (#3481)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 00:45:37 +00:00
dependabot[bot] a0a6761bfe
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.10.0 to 1.10.1 (#3479)
build(deps): bump actions-rust-lang/setup-rust-toolchain

Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.10.0...v1.10.1)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 00:44:29 +00:00
dependabot[bot] ff9c0f7157
build(deps): bump taiki-e/install-action from 2.44.15 to 2.44.24 (#3480)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.44.15 to 2.44.24.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.44.15...v2.44.24)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 00:44:11 +00:00
Bryan A. Jones 1c4e265a70
Set `SO_REUSEADDR` only non-Windows platforms (#3473)
* Fix: Per discussion in #2958, set `SO_REUSEADDR` only non-Windows platforms.

Add: Tests confirming that only a single webserver instance may
bind to a given address.

* Clean: Lint.

* Clean: another guess at making the formatter happy.

* Clean: More cargo fmt fun. (Running cargo fmt locally doesn't help.)

---------

Co-authored-by: Bryan A. Jones <bjones1@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-10-01 07:08:34 +00:00
dependabot[bot] d9d22825d4
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.9.0 to 1.10.0 (#3471)
build(deps): bump actions-rust-lang/setup-rust-toolchain

Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 04:32:19 +00:00
dependabot[bot] 9a685cabad
build(deps): bump taiki-e/install-action from 2.43.1 to 2.44.15 (#3476)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.43.1 to 2.44.15.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.43.1...v2.44.15)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 04:31:19 +00:00
Rob Ede 93edef8fee
ci: disable check-external-types job 2024-10-01 05:13:20 +01:00
Rob Ede d148e84aba
ci: fix nightly toolchain requirements 2024-10-01 05:01:32 +01:00
Rob Ede 7360c732b3
ci: downgrade parse-size for msrv 2024-10-01 04:50:09 +01:00
dependabot[bot] 48aaf41638
build(deps): bump taiki-e/install-action from 2.42.37 to 2.43.1 (#3465)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.42.37 to 2.43.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.42.37...v2.43.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-12 19:15:07 +00:00
dependabot[bot] bb13f54180
build(deps): bump taiki-e/install-action from 2.42.33 to 2.42.37 (#3464)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.42.33 to 2.42.37.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.42.33...v2.42.37)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-02 01:13:30 +00:00
dependabot[bot] b52e77beb4
build(deps): bump taiki-e/install-action from 2.42.26 to 2.42.33 (#3459)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.42.26 to 2.42.33.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.42.26...v2.42.33)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 01:00:33 +00:00
dependabot[bot] b4f8bda032
build(deps): bump taiki-e/install-action from 2.42.22 to 2.42.26 (#3456)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.42.22 to 2.42.26.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.42.22...v2.42.26)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 01:24:23 +00:00
Rob Ede c055723997
fix(awc): prevent panics in pool drop for h1 connections (#3448) 2024-08-18 15:54:36 +01:00
John Vandenberg d6bdfac1b9
build(deps): update derive_more to v1.0 (#3453)
* build(deps): update derive_more to v1.0

* refactor: use from derive module

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-08-18 14:17:03 +00:00
Rob Ede 78ac5cf482
docs(web): unmention try_init_service 2024-08-18 14:33:28 +01:00
Rob Ede 4303dd8c37
docs(web): mention try_init_service 2024-08-18 14:10:36 +01:00
dependabot[bot] f61fcbe840
build(deps): bump taiki-e/install-action from 2.42.17 to 2.42.22 (#3451)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.42.17 to 2.42.22.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.42.17...v2.42.22)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 12:12:48 +00:00
Rob Ede 538c1bea34
chore: disallow e bindings 2024-08-10 05:15:49 +01:00
Rob Ede 70e3758ecc
chore(awc): prepare release 3.5.1 2024-08-10 04:08:38 +01:00
Rob Ede 5ad92c0062
fix(awc): ws host req header includes port 2024-08-10 03:58:45 +01:00
Rob Ede e0918fb179
chore(actix-web): prepare release 4.9.0 2024-08-10 03:21:55 +01:00
Rob Ede 9ba326aed0
chore(actix-http): prepare release 3.9.0 2024-08-10 03:09:09 +01:00
Rob Ede 882fb3d25b
chore(actors): add version marker in changelog 2024-08-10 03:08:18 +01:00
Rob Ede be28a0bd6d
feat: add from_fn middleware (#3447) 2024-08-10 01:41:27 +01:00
Rob Ede a431b7356c
feat: add ThinData wrapper (#3446) 2024-08-10 00:42:34 +01:00
Rob Ede 5be53820f0
docs(actors): add maintenance badge 2024-08-07 04:32:16 +01:00
Rob Ede d7d9000b19
chore: address clippy warnings 2024-08-07 04:06:18 +01:00
Rob Ede e4e4bb799c
chore(actix-web-actors): prepare release 4.3.1 2024-08-07 04:02:30 +01:00
dependabot[bot] 323d1fa64f
build(deps): bump taiki-e/install-action from 2.42.9 to 2.42.17 (#3442)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.42.9 to 2.42.17.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.42.9...v2.42.17)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-07 01:05:32 +00:00
dependabot[bot] 9aa62112aa
build(deps): bump taiki-e/install-action from 2.42.4 to 2.42.9 (#3441)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.42.4 to 2.42.9.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.42.4...v2.42.9)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-29 00:25:30 +00:00
dependabot[bot] 270a6a3b70
build(deps): bump taiki-e/install-action from 2.41.17 to 2.42.4 (#3440)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.41.17 to 2.42.4.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.41.17...v2.42.4)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 00:49:25 +00:00
Onè 07f720f716
docs: fix typo (#3439) 2024-07-21 17:34:42 +00:00
dependabot[bot] f71f9ca66b
build(deps): bump taiki-e/install-action from 2.41.10 to 2.41.17 (#3431)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.41.10 to 2.41.17.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.41.10...v2.41.17)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-20 00:13:57 +00:00
dependabot[bot] b6bee346f7
build(deps): bump taiki-e/install-action from 2.41.7 to 2.41.10 (#3423)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.41.7 to 2.41.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.41.7...v2.41.10)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-08 09:31:50 +00:00
Rob Ede 5c6e0e17d3
feat(http): impl FromIter for HeaderMap 2024-07-07 21:16:25 +01:00
Rob Ede e97e28db4f
docs(multipart): improve crate root docs 2024-07-07 20:32:56 +01:00
Rob Ede 16125bd3be
docs(multipart): doc PayloadBuffer::readline 2024-07-07 20:19:56 +01:00
Rob Ede e9ccfbc866
refactor(multipart): clean up InnerField::poll 2024-07-07 20:19:35 +01:00
Rob Ede e0e4d1e661
chore: move deny lints to manifests 2024-07-07 03:54:00 +01:00
Rob Ede b01fbddba4
chore(actix-multipart): prepare release 0.7.2 2024-07-07 00:34:18 +01:00
Rob Ede 215a294584
chore(actix-multipart-derive): prepare release 0.7.0 2024-07-07 00:30:27 +01:00
Rob Ede ffee672909
chore(actix-multipart): prepare release 0.7.1 2024-07-07 00:19:22 +01:00
Rob Ede 01d60f3315
chore(actix-multipart): prepare release 0.7.0 2024-07-07 00:05:53 +01:00
Rob Ede 6ae131ce29
test(multipart): replace SlowStream helper 2024-07-06 23:38:37 +01:00
Rob Ede 5c9e6e7c1d
feat(multipart): add field bytes method 2024-07-06 22:58:54 +01:00
Rob Ede 611154beb2
refactor: rename multipart module 2024-07-04 05:03:42 +01:00
Rob Ede 210c9a5eb3
refactor: multipart tweaks 2024-07-04 04:53:10 +01:00
Rob Ede 00c185f617
refactor(multipart): move lints to manifest 2024-07-04 01:12:17 +01:00
Rob Ede 7326707599
refactor(multipart): move Field to module 2024-07-04 00:40:25 +01:00
Rob Ede befb9c8196
refactor(multipart): move Payload* to module 2024-07-04 00:37:25 +01:00
Rob Ede 2136e07bdd
refactor(multipart): move Safety to module 2024-07-04 00:26:10 +01:00
Piperck(Zhinan) e189e4a3bf
chore(awc): fix the issue where the code in the awc example cannot run (#3421) 2024-07-01 09:39:54 +00:00
Rob Ede 71cd3a31f9
fix(multipart): optional content-disposition for non-form-data requests (#3416) 2024-07-01 03:55:08 +01:00
dependabot[bot] 668b8e5745
build(deps): bump taiki-e/install-action from 2.41.2 to 2.41.7 (#3419)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.41.2 to 2.41.7.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.41.2...v2.41.7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 00:55:49 +00:00
Rob Ede 763c58445a
test: fix tests based on mime-guess inference
relates to https://github.com/abonander/mime_guess/pull/86
2024-06-30 20:28:11 +01:00
Rob Ede 0b193c7106
build: fix doc-watch recipe 2024-06-30 18:55:59 +01:00
Rob Ede 4db4251b8f
chore: cargo update after version bumps 2024-06-30 18:55:58 +01:00
dependabot[bot] 9f45be03e1
build(deps): bump taiki-e/install-action from 2.39.1 to 2.41.2 (#3412)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.39.1 to 2.41.2.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.39.1...v2.41.2)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 09:22:54 +00:00
Rob Ede 4222f92bd3
chore(actix-web): prepare release 4.8.0 2024-06-20 00:23:11 +01:00
Rob Ede d92a73eacd
chore(actix-http): prepare release 3.8.0 2024-06-20 00:18:22 +01:00
Rob Ede c612b5ce94
ci: fix checks 2024-06-20 00:13:42 +01:00
Rob Ede cbb55ba27d
ci: use just for feature combos check 2024-06-20 00:04:35 +01:00
Yury Yarashevich 643d64581a
Fix Rustls 0.22 & 0.23 are limited to 256 handshakes per second. (#3408) 2024-06-19 22:34:49 +00:00
dependabot[bot] 66905efd7b
build(deps): bump taiki-e/install-action from 2.38.0 to 2.39.1 (#3404)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.38.0 to 2.39.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.38.0...v2.39.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 02:28:03 +00:00
dependabot[bot] c076e34b5d
build(deps): bump codecov/codecov-action from 4.4.1 to 4.5.0 (#3405)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.4.1 to 4.5.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.4.1...v4.5.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 02:27:23 +00:00
dependabot[bot] 3ecaff5f5b
build(deps): bump taiki-e/cache-cargo-install-action from 1.2.2 to 2.0.1 (#3406)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 1.2.2 to 2.0.1.
- [Release notes](https://github.com/taiki-e/cache-cargo-install-action/releases)
- [Changelog](https://github.com/taiki-e/cache-cargo-install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/cache-cargo-install-action/compare/v1.2.2...v2.0.1)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 00:28:10 +00:00
Nikolaos Chatzikonstantinou fa74ab3dfb
remove references to StaticFiles (#3400) 2024-06-14 01:51:29 +00:00
Rob Ede 188206a903
feat: Html responder (#3399) 2024-06-11 00:36:46 +01:00
Rob Ede 0ce488e57a
docs: fix build 2024-06-10 23:54:16 +01:00
Rob Ede 132b84d3b1
docs(multipart): use cargo-rdme 2024-06-10 23:35:26 +01:00
Rob Ede cc5030c542
docs(http-test): use cargo-rdme 2024-06-10 23:31:45 +01:00
Rob Ede cd301a6932
docs: local docs doc everything but only list workspace crates 2024-06-10 23:30:51 +01:00
Rob Ede 4c4c279938
docs(test): intrgrate cargo-rdme 2024-06-10 23:23:38 +01:00
Rob Ede 0fd85bae2a
test: demonstrate panic in multipart forms (#3397) 2024-06-10 21:51:53 +01:00
Rob Ede 9b3de1f1fe
ci: fix doctest coverage 2024-06-10 04:15:58 +01:00
Rob Ede 9553e7afff
ci: fix coverage 2024-06-10 04:08:10 +01:00
Rob Ede d9579cf58a
test: coverage for doctests 2024-06-10 04:05:21 +01:00
Timo Caktu 7a2313cc4b
web: add `HttpRequest::full_url()` (#3096)
* implemented function which returns full uir

* changes added into the changelog

* added test funtion for full_uri method

* refactor: rename to full_url

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-10 02:49:50 +00:00
Rob Ede 2ee92d778e
ci: external types checking (#3175) 2024-06-10 03:39:06 +01:00
Matt Palmer 59e42c1446
Return 415 rather than 400 on Urlencoded Content-Type mismatch (#3334)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-10 01:19:35 +00:00
Rob Ede 53086a90a6
build: add coverage recipes to justfile 2024-06-10 01:58:16 +01:00
dependabot[bot] 7f529e35b2
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.8.0 to 1.9.0 (#3395)
build(deps): bump actions-rust-lang/setup-rust-toolchain

Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 00:45:11 +00:00
dependabot[bot] 4908fd7dea
build(deps): bump taiki-e/install-action from 2.34.0 to 2.38.0 (#3396)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.34.0 to 2.38.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.34.0...v2.38.0)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 00:44:58 +00:00
Matt Palmer a2b9823d9d
Strip non-address characters from Forwarded for= (#3343)
* Strip non-address characters from Forwarded for=

This is something of a followup to #2528, which asked for port information to not be included in  when it was taken from the local socket.

The  header's  element may optionally contain port information (https://datatracker.ietf.org/doc/html/rfc7239#section-6).
However, as I understand it,  is *supposed* to only contain an IP address, without port (per #2528).

This PR corrects that discrepancy, making it easier to parse the result of this method in application code.

There should not be any compatibility concerns, as anyone parsing the output of  would already need to handle both port and portless cases anyway.

* Update CHANGES.md

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-09 23:40:09 +00:00
Rob Ede da56de4556
chore(actix-test): prepare release 0.1.5 2024-06-10 00:01:17 +01:00
Matt Palmer 758ae1dac1
actix-test: allow the configuration of the TestServer address (#3351)
* actix-test: allow the configuration of the TestServer address

This is useful if you're running (say) Selenium tests against a running TestServer, and the Selenium workers are Docker containers elsewhere in the network.
Not a *particularly* common use case, perhaps, but one that I can attest happens every now and then.

* Update CHANGES.md

* Adjust default listen address to avoid test failures

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-09 19:07:08 +00:00
162 changed files with 8147 additions and 2261 deletions

View File

@ -1,10 +0,0 @@
[alias]
lint = "clippy --workspace --all-targets -- -Dclippy::todo"
lint-all = "clippy --workspace --all-features --all-targets -- -Dclippy::todo"
# lib checking
ci-check-min = "hack --workspace check --no-default-features"
ci-check-default = "hack --workspace check"
ci-check-default-tests = "check --workspace --tests"
ci-check-all-feature-powerset="hack --workspace --feature-powerset --depth=4 --skip=__compress,experimental-io-uring check"
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --depth=4 --skip=__compress check"

7
.clippy.toml Normal file
View File

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

3
.cspell.yml Normal file
View File

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

View File

@ -32,7 +32,7 @@ jobs:
- name: Install nasm - name: Install nasm
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.1 uses: ilammy/setup-nasm@v1.5.2
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
@ -44,20 +44,20 @@ jobs:
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.34.0 uses: taiki-e/install-action@v2.49.39
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
- name: check minimal - name: check minimal
run: cargo ci-check-min run: just check-min
- name: check default - name: check default
run: cargo ci-check-default run: just check-default
- name: tests - name: tests
timeout-minutes: 60 timeout-minutes: 60
@ -76,16 +76,16 @@ jobs:
- name: Free Disk Space - name: Free Disk Space
run: ./scripts/free-disk-space.sh run: ./scripts/free-disk-space.sh
- name: Setup mold linker
uses: rui314/setup-mold@v1
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
- name: Install cargo-hack - name: Install just, cargo-hack
uses: taiki-e/install-action@v2.34.0 uses: taiki-e/install-action@v2.49.39
with: with:
tool: cargo-hack tool: just,cargo-hack
- name: check feature combinations - name: Check feature combinations
run: cargo ci-check-all-feature-powerset run: just check-feature-combinations
- name: check feature combinations
run: cargo ci-check-all-feature-powerset-linux

View File

@ -43,7 +43,7 @@ jobs:
- name: Install nasm - name: Install nasm
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.1 uses: ilammy/setup-nasm@v1.5.2
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
@ -59,12 +59,12 @@ jobs:
uses: rui314/setup-mold@v1 uses: rui314/setup-mold@v1
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.34.0 uses: taiki-e/install-action@v2.49.39
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -73,10 +73,10 @@ jobs:
run: just downgrade-for-msrv run: just downgrade-for-msrv
- name: check minimal - name: check minimal
run: cargo ci-check-min run: just check-min
- name: check default - name: check default
run: cargo ci-check-default run: just check-default
- name: tests - name: tests
timeout-minutes: 60 timeout-minutes: 60
@ -92,7 +92,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
toolchain: nightly toolchain: nightly
@ -108,12 +108,12 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
toolchain: nightly toolchain: nightly
- name: Install just - name: Install just
uses: taiki-e/install-action@v2.34.0 uses: taiki-e/install-action@v2.49.39
with: with:
tool: just tool: just

View File

@ -17,21 +17,22 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
components: llvm-tools-preview toolchain: nightly
components: llvm-tools
- name: Install just,cargo-llvm-cov - name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@v2.34.0 uses: taiki-e/install-action@v2.49.39
with: with:
tool: just,cargo-llvm-cov tool: just,cargo-llvm-cov,cargo-nextest
- name: Generate code coverage - name: Generate code coverage
run: cargo llvm-cov --workspace --all-features --codecov --output-path codecov.json run: just test-coverage-codecov
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4.4.1 uses: codecov/codecov-action@v5.4.0
with: with:
files: codecov.json files: codecov.json
fail_ci_if_error: true fail_ci_if_error: true

View File

@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
toolchain: nightly toolchain: nightly
components: rustfmt components: rustfmt
@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
components: clippy components: clippy
@ -55,7 +55,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
toolchain: nightly toolchain: nightly
components: rust-docs components: rust-docs
@ -65,29 +65,26 @@ jobs:
RUSTDOCFLAGS: -D warnings RUSTDOCFLAGS: -D warnings
run: cargo +nightly doc --no-deps --workspace --all-features run: cargo +nightly doc --no-deps --workspace --all-features
public-api-diff: check-external-types:
if: false # rustdoc mismatch currently
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout main branch - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
ref: ${{ github.base_ref }} toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Checkout PR branch - name: Install just
uses: actions/checkout@v4 uses: taiki-e/install-action@v2.49.39
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: nightly-2024-06-07 tool: just
- name: Install cargo-public-api - name: Install cargo-check-external-types
uses: taiki-e/install-action@v2.34.0 uses: taiki-e/cache-cargo-install-action@v2.1.1
with: with:
tool: cargo-public-api tool: cargo-check-external-types
- name: Generate API diff - name: check external types
run: | run: just check-external-types-all +${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do
cargo public-api --manifest-path "$f" --simplified diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }}
done

1
.gitignore vendored
View File

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

3970
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.72" rust-version = "1.75"
[profile.dev] [profile.dev]
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
@ -51,3 +51,11 @@ awc = { path = "awc" }
# actix-utils = { path = "../actix-net/actix-utils" } # actix-utils = { path = "../actix-net/actix-utils" }
# actix-tls = { path = "../actix-net/actix-tls" } # actix-tls = { path = "../actix-net/actix-tls" }
# actix-server = { path = "../actix-net/actix-server" } # actix-server = { path = "../actix-net/actix-server" }
[workspace.lints.rust]
rust_2018_idioms = { level = "deny" }
future_incompatible = { level = "deny" }
nonstandard_style = { level = "deny" }
[workspace.lints.clippy]
# clone_on_ref_ptr = { level = "deny" }

View File

@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.6.6 ## 0.6.6
- Update `tokio-uring` dependency to `0.4`. - Update `tokio-uring` dependency to `0.4`.

View File

@ -13,9 +13,14 @@ categories = ["asynchronous", "web-programming::http-server"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[lib] [package.metadata.cargo_check_external_types]
name = "actix_files" allowed_external_types = [
path = "src/lib.rs" "actix_http::*",
"actix_service::*",
"actix_web::*",
"http::*",
"mime::*",
]
[features] [features]
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
@ -28,7 +33,7 @@ actix-web = { version = "4", default-features = false }
bitflags = "2" bitflags = "2"
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = { version = "2", features = ["display", "error", "from"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http-range = "0.1.4" http-range = "0.1.4"
log = "0.4" log = "0.4"
@ -49,3 +54,6 @@ actix-test = "0.1"
actix-web = "4" actix-web = "4"
env_logger = "0.11" env_logger = "0.11"
tempfile = "3.2" tempfile = "3.2"
[lints]
workspace = true

View File

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

View File

@ -11,8 +11,7 @@
//! .service(Files::new("/static", ".").prefer_utf8(true)); //! .service(Files::new("/static", ".").prefer_utf8(true));
//! ``` //! ```
#![deny(rust_2018_idioms, nonstandard_style)] #![warn(missing_docs, missing_debug_implementations)]
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
@ -307,11 +306,11 @@ mod tests {
let resp = file.respond_to(&req); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/javascript; charset=utf-8" "text/javascript",
); );
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"test.js\"" "inline; filename=\"test.js\"",
); );
} }

View File

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

View File

@ -18,9 +18,17 @@ edition = "2021"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [] features = []
[lib] [package.metadata.cargo_check_external_types]
name = "actix_http_test" allowed_external_types = [
path = "src/lib.rs" "actix_codec::*",
"actix_http::*",
"actix_server::*",
"awc::*",
"bytes::*",
"futures_core::*",
"http::*",
"tokio::*",
]
[features] [features]
default = [] default = []
@ -51,3 +59,6 @@ tokio = { version = "1.24.2", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-http = "3" actix-http = "3"
[lints]
workspace = true

View File

@ -1,7 +1,5 @@
# `actix-http-test` # `actix-http-test`
> Various helpers for Actix applications to use during testing.
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
@ -14,3 +12,9 @@
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
<!-- cargo-rdme start -->
Various helpers for Actix applications to use during testing.
<!-- cargo-rdme end -->

View File

@ -1,7 +1,5 @@
//! Various helpers for Actix applications to use during testing. //! Various helpers for Actix applications to use during testing.
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
@ -108,7 +106,7 @@ pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
builder.set_verify(SslVerifyMode::NONE); builder.set_verify(SslVerifyMode::NONE);
let _ = builder let _ = builder
.set_alpn_protos(b"\x02h2\x08http/1.1") .set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); .map_err(|err| log::error!("Can not set ALPN protocol: {err}"));
Connector::new() Connector::new()
.conn_lifetime(Duration::from_secs(0)) .conn_lifetime(Duration::from_secs(0))

View File

@ -2,6 +2,28 @@
## Unreleased ## Unreleased
## 3.10.0
### Added
- Add `header::CLEAR_SITE_DATA` constant.
- Add `Extensions::get_or_insert[_with]()` methods.
- Implement `From<Bytes>` for `Payload`.
- Implement `From<Vec<u8>>` for `Payload`.
### Changed
- Update `brotli` dependency to `7`.
- Minimum supported Rust version (MSRV) is now 1.75.
## 3.9.0
### Added
- Implement `FromIterator<(HeaderName, HeaderValue)>` for `HeaderMap`.
## 3.8.0
### Added ### Added
- Add `error::InvalidStatusCode` re-export. - Add `error::InvalidStatusCode` re-export.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.7.0" version = "3.10.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -34,63 +34,84 @@ features = [
"compress-zstd", "compress-zstd",
] ]
[lib] [package.metadata.cargo_check_external_types]
name = "actix_http" allowed_external_types = [
path = "src/lib.rs" "actix_codec::*",
"actix_service::*",
"actix_tls::*",
"actix_utils::*",
"bytes::*",
"bytestring::*",
"encoding_rs::*",
"futures_core::*",
"h2::*",
"http::*",
"httparse::*",
"language_tags::*",
"mime::*",
"openssl::*",
"rustls::*",
"tokio_util::*",
"tokio::*",
]
[features] [features]
default = [] default = []
# HTTP/2 protocol support # HTTP/2 protocol support
http2 = ["h2"] http2 = ["dep:h2"]
# WebSocket protocol implementation # WebSocket protocol implementation
ws = [ ws = [
"local-channel", "dep:local-channel",
"base64", "dep:base64",
"rand", "dep:rand",
"sha1", "dep:sha1",
] ]
# TLS via OpenSSL # TLS via OpenSSL
openssl = ["actix-tls/accept", "actix-tls/openssl"] openssl = ["__tls", "actix-tls/accept", "actix-tls/openssl"]
# TLS via Rustls v0.20 # TLS via Rustls v0.20
rustls = ["rustls-0_20"] rustls = ["__tls", "rustls-0_20"]
# TLS via Rustls v0.20 # TLS via Rustls v0.20
rustls-0_20 = ["actix-tls/accept", "actix-tls/rustls-0_20"] rustls-0_20 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_20"]
# TLS via Rustls v0.21 # TLS via Rustls v0.21
rustls-0_21 = ["actix-tls/accept", "actix-tls/rustls-0_21"] rustls-0_21 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_21"]
# TLS via Rustls v0.22 # TLS via Rustls v0.22
rustls-0_22 = ["actix-tls/accept", "actix-tls/rustls-0_22"] rustls-0_22 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_22"]
# TLS via Rustls v0.23 # TLS via Rustls v0.23
rustls-0_23 = ["actix-tls/accept", "actix-tls/rustls-0_23"] rustls-0_23 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_23"]
# Compression codecs # Compression codecs
compress-brotli = ["__compress", "brotli"] compress-brotli = ["__compress", "dep:brotli"]
compress-gzip = ["__compress", "flate2"] compress-gzip = ["__compress", "dep:flate2"]
compress-zstd = ["__compress", "zstd"] compress-zstd = ["__compress", "dep:zstd"]
# Internal (PRIVATE!) features used to aid testing and checking feature status. # Internal (PRIVATE!) features used to aid testing and checking feature status.
# Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime. # Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime.
__compress = [] __compress = []
# Internal (PRIVATE!) features used to aid checking feature status.
# Don't rely on these whatsoever. They may disappear at anytime.
__tls = []
[dependencies] [dependencies]
actix-service = "2" actix-service = "2"
actix-codec = "0.5" actix-codec = "0.5"
actix-utils = "3" actix-utils = "3"
actix-rt = { version = "2.2", default-features = false } actix-rt = { version = "2.2", default-features = false }
ahash = "0.8"
bitflags = "2" bitflags = "2"
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
derive_more = "0.99.5" derive_more = { version = "2", features = ["as_ref", "deref", "deref_mut", "display", "error", "from"] }
encoding_rs = "0.8" encoding_rs = "0.8"
foldhash = "0.1"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http = "0.2.7" http = "0.2.7"
httparse = "1.5.1" httparse = "1.5.1"
@ -111,14 +132,14 @@ h2 = { version = "0.3.26", optional = true }
# websockets # websockets
local-channel = { version = "0.1", optional = true } local-channel = { version = "0.1", optional = true }
base64 = { version = "0.22", optional = true } base64 = { version = "0.22", optional = true }
rand = { version = "0.8", optional = true } rand = { version = "0.9", optional = true }
sha1 = { version = "0.10", optional = true } sha1 = { version = "0.10", optional = true }
# openssl/rustls # openssl/rustls
actix-tls = { version = "3.4", default-features = false, optional = true } actix-tls = { version = "3.4", default-features = false, optional = true }
# compress-* # compress-*
brotli = { version = "6", optional = true } brotli = { version = "7", optional = true }
flate2 = { version = "1.0.13", optional = true } flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.13", optional = true } zstd = { version = "0.13", optional = true }
@ -134,18 +155,21 @@ divan = "0.1.8"
env_logger = "0.11" env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
memchr = "2.4" memchr = "2.4"
once_cell = "1.9" once_cell = "1.21"
rcgen = "0.13" rcgen = "0.13"
regex = "1.3" regex = "1.3"
rustversion = "1" rustversion = "1"
rustls-pemfile = "2" rustls-pemfile = "2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.55" } tls-openssl = { package = "openssl", version = "0.10.55" }
tls-rustls_023 = { package = "rustls", version = "0.23" } tls-rustls_023 = { package = "rustls", version = "0.23" }
tokio = { version = "1.24.2", features = ["net", "rt", "macros"] } tokio = { version = "1.24.2", features = ["net", "rt", "macros"] }
[lints]
workspace = true
[[example]] [[example]]
name = "ws" name = "ws"
required-features = ["ws", "rustls-0_23"] required-features = ["ws", "rustls-0_23"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,6 @@ use bytes::{Bytes, BytesMut};
use bytestring::ByteString; use bytestring::ByteString;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use tokio_util::codec::Encoder; use tokio_util::codec::Encoder;
use tracing::{info, trace};
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
@ -37,12 +36,12 @@ async fn main() -> io::Result<()> {
} }
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> { async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
info!("handshaking"); tracing::info!("handshaking");
let mut res = ws::handshake(req.head())?; let mut res = ws::handshake(req.head())?;
// handshake will always fail under HTTP/2 // handshake will always fail under HTTP/2
info!("responding"); tracing::info!("responding");
res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new()))) res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))
} }
@ -64,7 +63,7 @@ impl Stream for Heartbeat {
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
trace!("poll"); tracing::trace!("poll");
ready!(self.as_mut().interval.poll_tick(cx)); ready!(self.as_mut().interval.poll_tick(cx));

View File

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

View File

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

View File

@ -415,11 +415,11 @@ fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
#[non_exhaustive] #[non_exhaustive]
pub enum EncoderError { pub enum EncoderError {
/// Wrapped body stream error. /// Wrapped body stream error.
#[display(fmt = "body")] #[display("body")]
Body(Box<dyn StdError>), Body(Box<dyn StdError>),
/// Generic I/O error. /// Generic I/O error.
#[display(fmt = "io")] #[display("io")]
Io(io::Error), Io(io::Error),
} }

View File

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

View File

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

View File

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

View File

@ -480,15 +480,15 @@ where
let cfg = self.cfg.clone(); let cfg = self.cfg.clone();
Box::pin(async move { Box::pin(async move {
let expect = expect let expect = expect.await.map_err(|err| {
.await tracing::error!("Initialization of HTTP expect service error: {err:?}");
.map_err(|e| error!("Init http expect service error: {:?}", e))?; })?;
let upgrade = match upgrade { let upgrade = match upgrade {
Some(upgrade) => { Some(upgrade) => {
let upgrade = upgrade let upgrade = upgrade.await.map_err(|err| {
.await tracing::error!("Initialization of HTTP upgrade service error: {err:?}");
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?; })?;
Some(upgrade) Some(upgrade)
} }
None => None, None => None,
@ -496,7 +496,7 @@ where
let service = service let service = service
.await .await
.map_err(|e| error!("Init http service error: {:?}", e))?; .map_err(|err| error!("Initialization of HTTP service error: {err:?}"))?;
Ok(H1ServiceHandler::new( Ok(H1ServiceHandler::new(
cfg, cfg,
@ -541,6 +541,6 @@ where
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future { fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
Dispatcher::new(io, self.flow.clone(), self.cfg.clone(), addr, conn_data) Dispatcher::new(io, Rc::clone(&self.flow), self.cfg.clone(), addr, conn_data)
} }
} }

View File

@ -434,7 +434,7 @@ where
H2ServiceHandlerResponse { H2ServiceHandlerResponse {
state: State::Handshake( state: State::Handshake(
Some(self.flow.clone()), Some(Rc::clone(&self.flow)),
Some(self.cfg.clone()), Some(self.cfg.clone()),
addr, addr,
on_connect_data, on_connect_data,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,10 +6,10 @@
//! | ------------------- | ------------------------------------------- | //! | ------------------- | ------------------------------------------- |
//! | `http2` | HTTP/2 support via [h2]. | //! | `http2` | HTTP/2 support via [h2]. |
//! | `openssl` | TLS support via [OpenSSL]. | //! | `openssl` | TLS support via [OpenSSL]. |
//! | `rustls` | TLS support via [rustls] 0.20. | //! | `rustls-0_20` | TLS support via rustls 0.20. |
//! | `rustls-0_21` | TLS support via [rustls] 0.21. | //! | `rustls-0_21` | TLS support via rustls 0.21. |
//! | `rustls-0_22` | TLS support via [rustls] 0.22. | //! | `rustls-0_22` | TLS support via rustls 0.22. |
//! | `rustls-0_23` | TLS support via [rustls] 0.23. | //! | `rustls-0_23` | TLS support via [rustls] 0.23. |
//! | `compress-brotli` | Payload compression support: Brotli. | //! | `compress-brotli` | Payload compression support: Brotli. |
//! | `compress-gzip` | Payload compression support: Deflate, Gzip. | //! | `compress-gzip` | Payload compression support: Deflate, Gzip. |
//! | `compress-zstd` | Payload compression support: Zstd. | //! | `compress-zstd` | Payload compression support: Zstd. |
@ -20,8 +20,6 @@
//! [rustls]: https://crates.io/crates/rustls //! [rustls]: https://crates.io/crates/rustls
//! [trust-dns]: https://crates.io/crates/trust-dns //! [trust-dns]: https://crates.io/crates/trust-dns
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow( #![allow(
clippy::type_complexity, clippy::type_complexity,
clippy::too_many_arguments, clippy::too_many_arguments,
@ -61,13 +59,7 @@ pub mod ws;
#[allow(deprecated)] #[allow(deprecated)]
pub use self::payload::PayloadStream; pub use self::payload::PayloadStream;
#[cfg(any( #[cfg(feature = "__tls")]
feature = "openssl",
feature = "rustls-0_20",
feature = "rustls-0_21",
feature = "rustls-0_22",
feature = "rustls-0_23",
))]
pub use self::service::TlsAcceptorConfig; pub use self::service::TlsAcceptorConfig;
pub use self::{ pub use self::{
builder::HttpServiceBuilder, builder::HttpServiceBuilder,

View File

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

View File

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

View File

@ -351,12 +351,9 @@ mod tests {
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain"); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain");
let resp = Response::build(StatusCode::OK) let resp = Response::build(StatusCode::OK)
.content_type(mime::APPLICATION_JAVASCRIPT_UTF_8) .content_type(mime::TEXT_JAVASCRIPT)
.body(Bytes::new()); .body(Bytes::new());
assert_eq!( assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/javascript");
resp.headers().get(CONTENT_TYPE).unwrap(),
"application/javascript; charset=utf-8"
);
} }
#[test] #[test]

View File

@ -241,25 +241,13 @@ where
} }
/// Configuration options used when accepting TLS connection. /// Configuration options used when accepting TLS connection.
#[cfg(any( #[cfg(feature = "__tls")]
feature = "openssl",
feature = "rustls-0_20",
feature = "rustls-0_21",
feature = "rustls-0_22",
feature = "rustls-0_23",
))]
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct TlsAcceptorConfig { pub struct TlsAcceptorConfig {
pub(crate) handshake_timeout: Option<std::time::Duration>, pub(crate) handshake_timeout: Option<std::time::Duration>,
} }
#[cfg(any( #[cfg(feature = "__tls")]
feature = "openssl",
feature = "rustls-0_20",
feature = "rustls-0_21",
feature = "rustls-0_22",
feature = "rustls-0_23",
))]
impl TlsAcceptorConfig { impl TlsAcceptorConfig {
/// Set TLS handshake timeout duration. /// Set TLS handshake timeout duration.
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self { pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
@ -787,23 +775,23 @@ where
let cfg = self.cfg.clone(); let cfg = self.cfg.clone();
Box::pin(async move { Box::pin(async move {
let expect = expect let expect = expect.await.map_err(|err| {
.await tracing::error!("Initialization of HTTP expect service error: {err:?}");
.map_err(|e| error!("Init http expect service error: {:?}", e))?; })?;
let upgrade = match upgrade { let upgrade = match upgrade {
Some(upgrade) => { Some(upgrade) => {
let upgrade = upgrade let upgrade = upgrade.await.map_err(|err| {
.await tracing::error!("Initialization of HTTP upgrade service error: {err:?}");
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?; })?;
Some(upgrade) Some(upgrade)
} }
None => None, None => None,
}; };
let service = service let service = service.await.map_err(|err| {
.await tracing::error!("Initialization of HTTP service error: {err:?}");
.map_err(|e| error!("Init http service error: {:?}", e))?; })?;
Ok(HttpServiceHandler::new( Ok(HttpServiceHandler::new(
cfg, cfg,
@ -922,7 +910,7 @@ where
handshake: Some(( handshake: Some((
crate::h2::handshake_with_timeout(io, &self.cfg), crate::h2::handshake_with_timeout(io, &self.cfg),
self.cfg.clone(), self.cfg.clone(),
self.flow.clone(), Rc::clone(&self.flow),
conn_data, conn_data,
peer_addr, peer_addr,
)), )),
@ -938,7 +926,7 @@ where
state: State::H1 { state: State::H1 {
dispatcher: h1::Dispatcher::new( dispatcher: h1::Dispatcher::new(
io, io,
self.flow.clone(), Rc::clone(&self.flow),
self.cfg.clone(), self.cfg.clone(),
peer_addr, peer_addr,
conn_data, conn_data,

View File

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

View File

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

View File

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

View File

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

View File

@ -398,7 +398,7 @@ async fn h2_response_http_error_handling() {
} }
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display(fmt = "error")] #[display("error")]
struct BadRequest; struct BadRequest;
impl From<BadRequest> for Response<BoxBody> { impl From<BadRequest> for Response<BoxBody> {

View File

@ -480,7 +480,7 @@ async fn h2_response_http_error_handling() {
} }
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display(fmt = "error")] #[display("error")]
struct BadRequest; struct BadRequest;
impl From<BadRequest> for Response<BoxBody> { impl From<BadRequest> for Response<BoxBody> {

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart-derive" name = "actix-multipart-derive"
version = "0.6.1" version = "0.7.0"
authors = ["Jacob Halsey <jacob@jhalsey.com>"] authors = ["Jacob Halsey <jacob@jhalsey.com>"]
description = "Multipart form derive macro for Actix Web" description = "Multipart form derive macro for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@ -18,14 +18,17 @@ all-features = true
proc-macro = true proc-macro = true
[dependencies] [dependencies]
bytesize = "2"
darling = "0.20" darling = "0.20"
parse-size = "1"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = "2" syn = "2"
[dev-dependencies] [dev-dependencies]
actix-multipart = "0.6" actix-multipart = "0.7"
actix-web = "4" actix-web = "4"
rustversion = "1" rustversion-msrv = "0.100"
trybuild = "1" trybuild = "1"
[lints]
workspace = true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,37 +1,32 @@
# `actix-multipart` # `actix-multipart`
> Multipart form support for Actix Web.
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.6.2)](https://docs.rs/actix-multipart/0.6.2) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.7.2)](https://docs.rs/actix-multipart/0.7.2)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.6.2/status.svg)](https://deps.rs/crate/actix-multipart/0.6.2) [![dependency status](https://deps.rs/crate/actix-multipart/0.7.2/status.svg)](https://deps.rs/crate/actix-multipart/0.7.2)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
## Example <!-- cargo-rdme start -->
Dependencies: Multipart request & form support for Actix Web.
```toml The [`Multipart`] extractor aims to support all kinds of `multipart/*` requests, including `multipart/form-data`, `multipart/related` and `multipart/mixed`. This is a lower-level extractor which supports reading [multipart fields](Field), in the order they are sent by the client.
[dependencies]
actix-multipart = "0.6"
actix-web = "4.5"
serde = { version = "1.0", features = ["derive"] }
```
Code: Due to additional requirements for `multipart/form-data` requests, the higher level [`MultipartForm`] extractor and derive macro only supports this media type.
## Examples
```rust ```rust
use actix_web::{post, App, HttpServer, Responder}; use actix_web::{post, App, HttpServer, Responder};
use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm}; use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -43,7 +38,7 @@ struct Metadata {
struct UploadForm { struct UploadForm {
#[multipart(limit = "100MB")] #[multipart(limit = "100MB")]
file: TempFile, file: TempFile,
json: MPJson<Metadata>, json: MpJson<Metadata>,
} }
#[post("/videos")] #[post("/videos")]
@ -63,15 +58,17 @@ async fn main() -> std::io::Result<()> {
} }
``` ```
Curl request : cURL request:
```bash ```sh
curl -v --request POST \ curl -v --request POST \
--url http://localhost:8080/videos \ --url http://localhost:8080/videos \
-F 'json={"name": "Cargo.lock"};type=application/json' \ -F 'json={"name": "Cargo.lock"};type=application/json' \
-F file=@./Cargo.lock -F file=@./Cargo.lock
``` ```
### Examples [`MultipartForm`]: struct@form::MultipartForm
https://github.com/actix/examples/tree/master/forms/multipart <!-- cargo-rdme end -->
[More available in the examples repo &rarr;](https://github.com/actix/examples/tree/master/forms/multipart)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,6 @@ where
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future { fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future {
Box::pin(async move { Box::pin(async move {
let config = JsonConfig::from_req(req); let config = JsonConfig::from_req(req);
let field_name = field.name().to_owned();
if config.validate_content_type { if config.validate_content_type {
let valid = if let Some(mime) = field.content_type() { let valid = if let Some(mime) = field.content_type() {
@ -43,17 +42,19 @@ where
if !valid { if !valid {
return Err(MultipartError::Field { return Err(MultipartError::Field {
field_name, name: field.form_field_name,
source: config.map_error(req, JsonFieldError::ContentType), source: config.map_error(req, JsonFieldError::ContentType),
}); });
} }
} }
let form_field_name = field.form_field_name.clone();
let bytes = Bytes::read_field(req, field, limits).await?; let bytes = Bytes::read_field(req, field, limits).await?;
Ok(Json(serde_json::from_slice(bytes.data.as_ref()).map_err( Ok(Json(serde_json::from_slice(bytes.data.as_ref()).map_err(
|err| MultipartError::Field { |err| MultipartError::Field {
field_name, name: form_field_name,
source: config.map_error(req, JsonFieldError::Deserialize(err)), source: config.map_error(req, JsonFieldError::Deserialize(err)),
}, },
)?)) )?))
@ -65,11 +66,11 @@ where
#[non_exhaustive] #[non_exhaustive]
pub enum JsonFieldError { pub enum JsonFieldError {
/// Deserialize error. /// Deserialize error.
#[display(fmt = "Json deserialize error: {}", _0)] #[display("Json deserialize error: {}", _0)]
Deserialize(serde_json::Error), Deserialize(serde_json::Error),
/// Content type error. /// Content type error.
#[display(fmt = "Content type error")] #[display("Content type error")]
ContentType, ContentType,
} }
@ -133,8 +134,7 @@ impl Default for JsonConfig {
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder}; use actix_web::{http::StatusCode, web, web::Bytes, App, HttpResponse, Responder};
use bytes::Bytes;
use crate::form::{ use crate::form::{
json::{Json, JsonConfig}, json::{Json, JsonConfig},

View File

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

View File

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

View File

@ -36,7 +36,6 @@ where
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future { fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future {
Box::pin(async move { Box::pin(async move {
let config = TextConfig::from_req(req); let config = TextConfig::from_req(req);
let field_name = field.name().to_owned();
if config.validate_content_type { if config.validate_content_type {
let valid = if let Some(mime) = field.content_type() { let valid = if let Some(mime) = field.content_type() {
@ -49,22 +48,24 @@ where
if !valid { if !valid {
return Err(MultipartError::Field { return Err(MultipartError::Field {
field_name, name: field.form_field_name,
source: config.map_error(req, TextError::ContentType), source: config.map_error(req, TextError::ContentType),
}); });
} }
} }
let form_field_name = field.form_field_name.clone();
let bytes = Bytes::read_field(req, field, limits).await?; let bytes = Bytes::read_field(req, field, limits).await?;
let text = str::from_utf8(&bytes.data).map_err(|err| MultipartError::Field { let text = str::from_utf8(&bytes.data).map_err(|err| MultipartError::Field {
field_name: field_name.clone(), name: form_field_name.clone(),
source: config.map_error(req, TextError::Utf8Error(err)), source: config.map_error(req, TextError::Utf8Error(err)),
})?; })?;
Ok(Text(serde_plain::from_str(text).map_err(|err| { Ok(Text(serde_plain::from_str(text).map_err(|err| {
MultipartError::Field { MultipartError::Field {
field_name, name: form_field_name,
source: config.map_error(req, TextError::Deserialize(err)), source: config.map_error(req, TextError::Deserialize(err)),
} }
})?)) })?))
@ -76,15 +77,15 @@ where
#[non_exhaustive] #[non_exhaustive]
pub enum TextError { pub enum TextError {
/// UTF-8 decoding error. /// UTF-8 decoding error.
#[display(fmt = "UTF-8 decoding error: {}", _0)] #[display("UTF-8 decoding error: {}", _0)]
Utf8Error(str::Utf8Error), Utf8Error(str::Utf8Error),
/// Deserialize error. /// Deserialize error.
#[display(fmt = "Plain text deserialize error: {}", _0)] #[display("Plain text deserialize error: {}", _0)]
Deserialize(serde_plain::Error), Deserialize(serde_plain::Error),
/// Content type error. /// Content type error.
#[display(fmt = "Content type error")] #[display("Content type error")]
ContentType, ContentType,
} }

View File

@ -1,9 +1,19 @@
//! Multipart form support for Actix Web. //! Multipart request & form support for Actix Web.
//!
//! The [`Multipart`] extractor aims to support all kinds of `multipart/*` requests, including
//! `multipart/form-data`, `multipart/related` and `multipart/mixed`. This is a lower-level
//! extractor which supports reading [multipart fields](Field), in the order they are sent by the
//! client.
//!
//! Due to additional requirements for `multipart/form-data` requests, the higher level
//! [`MultipartForm`] extractor and derive macro only supports this media type.
//!
//! # Examples //! # Examples
//!
//! ```no_run //! ```no_run
//! use actix_web::{post, App, HttpServer, Responder}; //! use actix_web::{post, App, HttpServer, Responder};
//! //!
//! use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm}; //! use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
//! use serde::Deserialize; //! use serde::Deserialize;
//! //!
//! #[derive(Debug, Deserialize)] //! #[derive(Debug, Deserialize)]
@ -15,7 +25,7 @@
//! struct UploadForm { //! struct UploadForm {
//! #[multipart(limit = "100MB")] //! #[multipart(limit = "100MB")]
//! file: TempFile, //! file: TempFile,
//! json: MPJson<Metadata>, //! json: MpJson<Metadata>,
//! } //! }
//! //!
//! #[post("/videos")] //! #[post("/videos")]
@ -34,10 +44,18 @@
//! .await //! .await
//! } //! }
//! ``` //! ```
//!
//! cURL request:
//!
//! ```sh
//! curl -v --request POST \
//! --url http://localhost:8080/videos \
//! -F 'json={"name": "Cargo.lock"};type=application/json' \
//! -F file=@./Cargo.lock
//! ```
//!
//! [`MultipartForm`]: struct@form::MultipartForm
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow(clippy::borrow_interior_mutable_const)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
@ -48,14 +66,15 @@ extern crate self as actix_multipart;
mod error; mod error;
mod extractor; mod extractor;
pub(crate) mod field;
pub mod form; pub mod form;
mod server; mod multipart;
pub(crate) mod payload;
pub(crate) mod safety;
pub mod test; pub mod test;
pub use self::{ pub use self::{
error::MultipartError, error::Error as MultipartError,
server::{Field, Multipart}, field::{Field, LimitExceeded},
test::{ multipart::Multipart,
create_form_data_payload_and_headers, create_form_data_payload_and_headers_with_boundary,
},
}; };

View File

@ -0,0 +1,883 @@
//! Multipart response payload support.
use std::{
cell::RefCell,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_web::{
dev,
error::{ParseError, PayloadError},
http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue},
web::Bytes,
HttpRequest,
};
use futures_core::stream::Stream;
use mime::Mime;
use crate::{
error::Error,
field::InnerField,
payload::{PayloadBuffer, PayloadRef},
safety::Safety,
Field,
};
const MAX_HEADERS: usize = 32;
/// The server-side implementation of `multipart/form-data` requests.
///
/// This will parse the incoming stream into `MultipartItem` instances via its `Stream`
/// implementation. `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart` is
/// used for nested multipart streams.
pub struct Multipart {
flow: Flow,
safety: Safety,
}
enum Flow {
InFlight(Inner),
/// Error container is Some until an error is returned out of the flow.
Error(Option<Error>),
}
impl Multipart {
/// Creates multipart instance from parts.
pub fn new<S>(headers: &HeaderMap, stream: S) -> Self
where
S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{
match Self::find_ct_and_boundary(headers) {
Ok((ct, boundary)) => Self::from_ct_and_boundary(ct, boundary, stream),
Err(err) => Self::from_error(err),
}
}
/// Creates multipart instance from parts.
pub(crate) fn from_req(req: &HttpRequest, payload: &mut dev::Payload) -> Self {
match Self::find_ct_and_boundary(req.headers()) {
Ok((ct, boundary)) => Self::from_ct_and_boundary(ct, boundary, payload.take()),
Err(err) => Self::from_error(err),
}
}
/// Extract Content-Type and boundary info from headers.
pub(crate) fn find_ct_and_boundary(headers: &HeaderMap) -> Result<(Mime, String), Error> {
let content_type = headers
.get(&header::CONTENT_TYPE)
.ok_or(Error::ContentTypeMissing)?
.to_str()
.ok()
.and_then(|content_type| content_type.parse::<Mime>().ok())
.ok_or(Error::ContentTypeParse)?;
if content_type.type_() != mime::MULTIPART {
return Err(Error::ContentTypeIncompatible);
}
let boundary = content_type
.get_param(mime::BOUNDARY)
.ok_or(Error::BoundaryMissing)?
.as_str()
.to_owned();
Ok((content_type, boundary))
}
/// Constructs a new multipart reader from given Content-Type, boundary, and stream.
pub(crate) fn from_ct_and_boundary<S>(ct: Mime, boundary: String, stream: S) -> Multipart
where
S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{
Multipart {
safety: Safety::new(),
flow: Flow::InFlight(Inner {
payload: PayloadRef::new(PayloadBuffer::new(stream)),
content_type: ct,
boundary,
state: State::FirstBoundary,
item: Item::None,
}),
}
}
/// Constructs a new multipart reader from given `MultipartError`.
pub(crate) fn from_error(err: Error) -> Multipart {
Multipart {
flow: Flow::Error(Some(err)),
safety: Safety::new(),
}
}
/// Return requests parsed Content-Type or raise the stored error.
pub(crate) fn content_type_or_bail(&mut self) -> Result<mime::Mime, Error> {
match self.flow {
Flow::InFlight(ref inner) => Ok(inner.content_type.clone()),
Flow::Error(ref mut err) => Err(err
.take()
.expect("error should not be taken after it was returned")),
}
}
}
impl Stream for Multipart {
type Item = Result<Field, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
match this.flow {
Flow::InFlight(ref mut inner) => {
if let Some(mut buffer) = inner.payload.get_mut(&this.safety) {
// check safety and poll read payload to buffer.
buffer.poll_stream(cx)?;
} else if !this.safety.is_clean() {
// safety violation
return Poll::Ready(Some(Err(Error::NotConsumed)));
} else {
return Poll::Pending;
}
inner.poll(&this.safety, cx)
}
Flow::Error(ref mut err) => Poll::Ready(Some(Err(err
.take()
.expect("Multipart polled after finish")))),
}
}
}
#[derive(PartialEq, Debug)]
enum State {
/// Skip data until first boundary.
FirstBoundary,
/// Reading boundary.
Boundary,
/// Reading Headers.
Headers,
/// Stream EOF.
Eof,
}
enum Item {
None,
Field(Rc<RefCell<InnerField>>),
}
struct Inner {
/// Request's payload stream & buffer.
payload: PayloadRef,
/// Request's Content-Type.
///
/// Guaranteed to have "multipart" top-level media type, i.e., `multipart/*`.
content_type: Mime,
/// Field boundary.
boundary: String,
state: State,
item: Item,
}
impl Inner {
fn read_field_headers(payload: &mut PayloadBuffer) -> Result<Option<HeaderMap>, Error> {
match payload.read_until(b"\r\n\r\n")? {
None => {
if payload.eof {
Err(Error::Incomplete)
} else {
Ok(None)
}
}
Some(bytes) => {
let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS];
match httparse::parse_headers(&bytes, &mut hdrs).map_err(ParseError::from)? {
httparse::Status::Complete((_, hdrs)) => {
// convert headers
let mut headers = HeaderMap::with_capacity(hdrs.len());
for h in hdrs {
let name =
HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?;
let value =
HeaderValue::try_from(h.value).map_err(|_| ParseError::Header)?;
headers.append(name, value);
}
Ok(Some(headers))
}
httparse::Status::Partial => Err(ParseError::Header.into()),
}
}
}
}
/// Reads a field boundary from the payload buffer (and discards it).
///
/// Reads "in-between" and "final" boundaries. E.g. for boundary = "foo":
///
/// ```plain
/// --foo <-- in-between fields
/// --foo-- <-- end of request body, should be followed by EOF
/// ```
///
/// Returns:
///
/// - `Ok(Some(true))` - final field boundary read (EOF)
/// - `Ok(Some(false))` - field boundary read
/// - `Ok(None)` - boundary not found, more data needs reading
/// - `Err(BoundaryMissing)` - multipart boundary is missing
fn read_boundary(payload: &mut PayloadBuffer, boundary: &str) -> Result<Option<bool>, Error> {
// TODO: need to read epilogue
let chunk = match payload.readline_or_eof()? {
// TODO: this might be okay as a let Some() else return Ok(None)
None => return Ok(payload.eof.then_some(true)),
Some(chunk) => chunk,
};
const BOUNDARY_MARKER: &[u8] = b"--";
const LINE_BREAK: &[u8] = b"\r\n";
let boundary_len = boundary.len();
if chunk.len() < boundary_len + 2 + 2
|| !chunk.starts_with(BOUNDARY_MARKER)
|| &chunk[2..boundary_len + 2] != boundary.as_bytes()
{
return Err(Error::BoundaryMissing);
}
// chunk facts:
// - long enough to contain boundary + 2 markers or 1 marker and line-break
// - starts with boundary marker
// - chunk contains correct boundary
if &chunk[boundary_len + 2..] == LINE_BREAK {
// boundary is followed by line-break, indicating more fields to come
return Ok(Some(false));
}
// boundary is followed by marker
if &chunk[boundary_len + 2..boundary_len + 4] == BOUNDARY_MARKER
&& (
// chunk is exactly boundary len + 2 markers
chunk.len() == boundary_len + 2 + 2
// final boundary is allowed to end with a line-break
|| &chunk[boundary_len + 4..] == LINE_BREAK
)
{
return Ok(Some(true));
}
Err(Error::BoundaryMissing)
}
fn skip_until_boundary(
payload: &mut PayloadBuffer,
boundary: &str,
) -> Result<Option<bool>, Error> {
let mut eof = false;
loop {
match payload.readline()? {
Some(chunk) => {
if chunk.is_empty() {
return Err(Error::BoundaryMissing);
}
if chunk.len() < boundary.len() {
continue;
}
if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() {
break;
} else {
if chunk.len() < boundary.len() + 2 {
continue;
}
let b: &[u8] = boundary.as_ref();
if &chunk[..boundary.len()] == b
&& &chunk[boundary.len()..boundary.len() + 2] == b"--"
{
eof = true;
break;
}
}
}
None => {
return if payload.eof {
Err(Error::Incomplete)
} else {
Ok(None)
};
}
}
}
Ok(Some(eof))
}
fn poll(&mut self, safety: &Safety, cx: &Context<'_>) -> Poll<Option<Result<Field, Error>>> {
if self.state == State::Eof {
Poll::Ready(None)
} else {
// release field
loop {
// Nested multipart streams of fields has to be consumed
// before switching to next
if safety.current() {
let stop = match self.item {
Item::Field(ref mut field) => match field.borrow_mut().poll(safety) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(_))) => continue,
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
Poll::Ready(None) => true,
},
Item::None => false,
};
if stop {
self.item = Item::None;
}
if let Item::None = self.item {
break;
}
}
}
let field_headers = if let Some(mut payload) = self.payload.get_mut(safety) {
match self.state {
// read until first boundary
State::FirstBoundary => {
match Inner::skip_until_boundary(&mut payload, &self.boundary)? {
None => return Poll::Pending,
Some(eof) => {
if eof {
self.state = State::Eof;
return Poll::Ready(None);
} else {
self.state = State::Headers;
}
}
}
}
// read boundary
State::Boundary => match Inner::read_boundary(&mut payload, &self.boundary)? {
None => return Poll::Pending,
Some(eof) => {
if eof {
self.state = State::Eof;
return Poll::Ready(None);
} else {
self.state = State::Headers;
}
}
},
_ => {}
}
// read field headers for next field
if self.state == State::Headers {
if let Some(headers) = Inner::read_field_headers(&mut payload)? {
self.state = State::Boundary;
headers
} else {
return Poll::Pending;
}
} else {
unreachable!()
}
} else {
log::debug!("NotReady: field is in flight");
return Poll::Pending;
};
let field_content_disposition = field_headers
.get(&header::CONTENT_DISPOSITION)
.and_then(|cd| ContentDisposition::from_raw(cd).ok())
.filter(|content_disposition| {
matches!(
content_disposition.disposition,
header::DispositionType::FormData,
)
});
let form_field_name = if self.content_type.subtype() == mime::FORM_DATA {
// According to RFC 7578 §4.2, which relates to "multipart/form-data" requests
// specifically, fields must have a Content-Disposition header, its disposition
// type must be set as "form-data", and it must have a name parameter.
let Some(cd) = &field_content_disposition else {
return Poll::Ready(Some(Err(Error::ContentDispositionMissing)));
};
let Some(field_name) = cd.get_name() else {
return Poll::Ready(Some(Err(Error::ContentDispositionNameMissing)));
};
Some(field_name.to_owned())
} else {
None
};
// TODO: check out other multipart/* RFCs for specific requirements
let field_content_type: Option<Mime> = field_headers
.get(&header::CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.and_then(|ct| ct.parse().ok());
self.state = State::Boundary;
// nested multipart stream is not supported
if let Some(mime) = &field_content_type {
if mime.type_() == mime::MULTIPART {
return Poll::Ready(Some(Err(Error::Nested)));
}
}
let field_inner =
InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &field_headers)?;
self.item = Item::Field(Rc::clone(&field_inner));
Poll::Ready(Some(Ok(Field::new(
field_content_type,
field_content_disposition,
form_field_name,
field_headers,
safety.clone(cx),
field_inner,
))))
}
}
}
impl Drop for Inner {
fn drop(&mut self) {
// InnerMultipartItem::Field has to be dropped first because of Safety.
self.item = Item::None;
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use actix_http::h1;
use actix_web::{
http::header::{DispositionParam, DispositionType},
rt,
test::TestRequest,
web::{BufMut as _, BytesMut},
FromRequest,
};
use assert_matches::assert_matches;
use futures_test::stream::StreamTestExt as _;
use futures_util::{stream, StreamExt as _};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use super::*;
const BOUNDARY: &str = "abbc761f78ff4d7cb7573b5a23f96ef0";
#[actix_rt::test]
async fn test_boundary() {
let headers = HeaderMap::new();
match Multipart::find_ct_and_boundary(&headers) {
Err(Error::ContentTypeMissing) => {}
_ => unreachable!("should not happen"),
}
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("test"),
);
match Multipart::find_ct_and_boundary(&headers) {
Err(Error::ContentTypeParse) => {}
_ => unreachable!("should not happen"),
}
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("multipart/mixed"),
);
match Multipart::find_ct_and_boundary(&headers) {
Err(Error::BoundaryMissing) => {}
_ => unreachable!("should not happen"),
}
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"",
),
);
assert_eq!(
Multipart::find_ct_and_boundary(&headers).unwrap().1,
"5c02368e880e436dab70ed54e1c58209",
);
}
fn create_stream() -> (
mpsc::UnboundedSender<Result<Bytes, PayloadError>>,
impl Stream<Item = Result<Bytes, PayloadError>>,
) {
let (tx, rx) = mpsc::unbounded_channel();
(
tx,
UnboundedReceiverStream::new(rx).map(|res| res.map_err(|_| panic!())),
)
}
fn create_simple_request_with_header() -> (Bytes, HeaderMap) {
let (body, headers) = crate::test::create_form_data_payload_and_headers_with_boundary(
BOUNDARY,
"file",
Some("fn.txt".to_owned()),
Some(mime::TEXT_PLAIN_UTF_8),
Bytes::from_static(b"data"),
);
let mut buf = BytesMut::with_capacity(body.len() + 14);
// add junk before form to test pre-boundary data rejection
buf.put("testasdadsad\r\n".as_bytes());
buf.put(body);
(buf.freeze(), headers)
}
// TODO: use test utility when multi-file support is introduced
fn create_double_request_with_header() -> (Bytes, HeaderMap) {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
data\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
(bytes, headers)
}
#[actix_rt::test]
async fn test_multipart_no_end_crlf() {
let (sender, payload) = create_stream();
let (mut bytes, headers) = create_double_request_with_header();
let bytes_stripped = bytes.split_to(bytes.len()); // strip crlf
sender.send(Ok(bytes_stripped)).unwrap();
drop(sender); // eof
let mut multipart = Multipart::new(&headers, payload);
match multipart.next().await.unwrap() {
Ok(_) => {}
_ => unreachable!(),
}
match multipart.next().await.unwrap() {
Ok(_) => {}
_ => unreachable!(),
}
match multipart.next().await {
None => {}
_ => unreachable!(),
}
}
#[actix_rt::test]
async fn test_multipart() {
let (sender, payload) = create_stream();
let (bytes, headers) = create_double_request_with_header();
sender.send(Ok(bytes)).unwrap();
let mut multipart = Multipart::new(&headers, payload);
match multipart.next().await {
Some(Ok(mut field)) => {
let cd = field.content_disposition().unwrap();
assert_eq!(cd.disposition, DispositionType::FormData);
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
match field.next().await.unwrap() {
Ok(chunk) => assert_eq!(chunk, "test"),
_ => unreachable!(),
}
match field.next().await {
None => {}
_ => unreachable!(),
}
}
_ => unreachable!(),
}
match multipart.next().await.unwrap() {
Ok(mut field) => {
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
match field.next().await {
Some(Ok(chunk)) => assert_eq!(chunk, "data"),
_ => unreachable!(),
}
match field.next().await {
None => {}
_ => unreachable!(),
}
}
_ => unreachable!(),
}
match multipart.next().await {
None => {}
_ => unreachable!(),
}
}
// Loops, collecting all bytes until end-of-field
async fn get_whole_field(field: &mut Field) -> BytesMut {
let mut b = BytesMut::new();
loop {
match field.next().await {
Some(Ok(chunk)) => b.extend_from_slice(&chunk),
None => return b,
_ => unreachable!(),
}
}
}
#[actix_rt::test]
async fn test_stream() {
let (bytes, headers) = create_double_request_with_header();
let payload = stream::iter(bytes)
.map(|byte| Ok(Bytes::copy_from_slice(&[byte])))
.interleave_pending();
let mut multipart = Multipart::new(&headers, payload);
match multipart.next().await.unwrap() {
Ok(mut field) => {
let cd = field.content_disposition().unwrap();
assert_eq!(cd.disposition, DispositionType::FormData);
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
assert_eq!(get_whole_field(&mut field).await, "test");
}
_ => unreachable!(),
}
match multipart.next().await {
Some(Ok(mut field)) => {
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
assert_eq!(get_whole_field(&mut field).await, "data");
}
_ => unreachable!(),
}
match multipart.next().await {
None => {}
_ => unreachable!(),
}
}
#[actix_rt::test]
async fn test_multipart_from_error() {
let err = Error::ContentTypeMissing;
let mut multipart = Multipart::from_error(err);
assert!(multipart.next().await.unwrap().is_err())
}
#[actix_rt::test]
async fn test_multipart_from_boundary() {
let (_, payload) = create_stream();
let (_, headers) = create_simple_request_with_header();
let (ct, boundary) = Multipart::find_ct_and_boundary(&headers).unwrap();
let _ = Multipart::from_ct_and_boundary(ct, boundary, payload);
}
#[actix_rt::test]
async fn test_multipart_payload_consumption() {
// with sample payload and HttpRequest with no headers
let (_, inner_payload) = h1::Payload::create(false);
let mut payload = actix_web::dev::Payload::from(inner_payload);
let req = TestRequest::default().to_http_request();
// multipart should generate an error
let mut mp = Multipart::from_request(&req, &mut payload).await.unwrap();
assert!(mp.next().await.unwrap().is_err());
// and should not consume the payload
match payload {
actix_web::dev::Payload::H1 { .. } => {} //expected
_ => unreachable!(),
}
}
#[actix_rt::test]
async fn no_content_disposition_form_data() {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
Content-Length: 4\r\n\
\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
let payload = stream::iter(bytes)
.map(|byte| Ok(Bytes::copy_from_slice(&[byte])))
.interleave_pending();
let mut multipart = Multipart::new(&headers, payload);
let res = multipart.next().await.unwrap();
assert_matches!(
res.expect_err(
"according to RFC 7578, form-data fields require a content-disposition header"
),
Error::ContentDispositionMissing
);
}
#[actix_rt::test]
async fn no_content_disposition_non_form_data() {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
Content-Length: 4\r\n\
\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
let payload = stream::iter(bytes)
.map(|byte| Ok(Bytes::copy_from_slice(&[byte])))
.interleave_pending();
let mut multipart = Multipart::new(&headers, payload);
let res = multipart.next().await.unwrap();
res.unwrap();
}
#[actix_rt::test]
async fn no_name_in_form_data_content_disposition() {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
Content-Length: 4\r\n\
\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
let payload = stream::iter(bytes)
.map(|byte| Ok(Bytes::copy_from_slice(&[byte])))
.interleave_pending();
let mut multipart = Multipart::new(&headers, payload);
let res = multipart.next().await.unwrap();
assert_matches!(
res.expect_err("according to RFC 7578, form-data fields require a name attribute"),
Error::ContentDispositionNameMissing
);
}
#[actix_rt::test]
async fn test_drop_multipart_dont_hang() {
let (sender, payload) = create_stream();
let (bytes, headers) = create_simple_request_with_header();
sender.send(Ok(bytes)).unwrap();
drop(sender); // eof
let mut multipart = Multipart::new(&headers, payload);
let mut field = multipart.next().await.unwrap().unwrap();
drop(multipart);
// should fail immediately
match field.next().await {
Some(Err(Error::NotConsumed)) => {}
_ => panic!(),
};
}
#[actix_rt::test]
async fn test_drop_field_awaken_multipart() {
let (sender, payload) = create_stream();
let (bytes, headers) = create_double_request_with_header();
sender.send(Ok(bytes)).unwrap();
drop(sender); // eof
let mut multipart = Multipart::new(&headers, payload);
let mut field = multipart.next().await.unwrap().unwrap();
let task = rt::spawn(async move {
rt::time::sleep(Duration::from_millis(500)).await;
assert_eq!(field.next().await.unwrap().unwrap(), "test");
drop(field);
});
// dropping field should awaken current task
let _ = multipart.next().await.unwrap().unwrap();
task.await.unwrap();
}
}

View File

@ -0,0 +1,255 @@
use std::{
cell::{RefCell, RefMut},
cmp, mem,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_web::{
error::PayloadError,
web::{Bytes, BytesMut},
};
use futures_core::stream::{LocalBoxStream, Stream};
use crate::{error::Error, safety::Safety};
pub(crate) struct PayloadRef {
payload: Rc<RefCell<PayloadBuffer>>,
}
impl PayloadRef {
pub(crate) fn new(payload: PayloadBuffer) -> PayloadRef {
PayloadRef {
payload: Rc::new(RefCell::new(payload)),
}
}
pub(crate) fn get_mut(&self, safety: &Safety) -> Option<RefMut<'_, PayloadBuffer>> {
if safety.current() {
Some(self.payload.borrow_mut())
} else {
None
}
}
}
impl Clone for PayloadRef {
fn clone(&self) -> PayloadRef {
PayloadRef {
payload: Rc::clone(&self.payload),
}
}
}
/// Payload buffer.
pub(crate) struct PayloadBuffer {
pub(crate) stream: LocalBoxStream<'static, Result<Bytes, PayloadError>>,
pub(crate) buf: BytesMut,
/// EOF flag. If true, no more payload reads will be attempted.
pub(crate) eof: bool,
}
impl PayloadBuffer {
/// Constructs new payload buffer.
pub(crate) fn new<S>(stream: S) -> Self
where
S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{
PayloadBuffer {
stream: Box::pin(stream),
buf: BytesMut::with_capacity(1_024), // pre-allocate 1KiB
eof: false,
}
}
pub(crate) fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> {
loop {
match Pin::new(&mut self.stream).poll_next(cx) {
Poll::Ready(Some(Ok(data))) => {
self.buf.extend_from_slice(&data);
// try to read more data
continue;
}
Poll::Ready(Some(Err(err))) => return Err(err),
Poll::Ready(None) => {
self.eof = true;
return Ok(());
}
Poll::Pending => return Ok(()),
}
}
}
/// Reads exact number of bytes.
#[cfg(test)]
pub(crate) fn read_exact(&mut self, size: usize) -> Option<Bytes> {
if size <= self.buf.len() {
Some(self.buf.split_to(size).freeze())
} else {
None
}
}
pub(crate) fn read_max(&mut self, size: u64) -> Result<Option<Bytes>, Error> {
if !self.buf.is_empty() {
let size = cmp::min(self.buf.len() as u64, size) as usize;
Ok(Some(self.buf.split_to(size).freeze()))
} else if self.eof {
Err(Error::Incomplete)
} else {
Ok(None)
}
}
/// Reads until specified ending.
///
/// Returns:
///
/// - `Ok(Some(chunk))` - `needle` is found, with chunk ending after needle
/// - `Err(Incomplete)` - `needle` is not found and we're at EOF
/// - `Ok(None)` - `needle` is not found otherwise
pub(crate) fn read_until(&mut self, needle: &[u8]) -> Result<Option<Bytes>, Error> {
match memchr::memmem::find(&self.buf, needle) {
// buffer exhausted and EOF without finding needle
None if self.eof => Err(Error::Incomplete),
// needle not yet found
None => Ok(None),
// needle found, split chunk out of buf
Some(idx) => Ok(Some(self.buf.split_to(idx + needle.len()).freeze())),
}
}
/// Reads bytes until new line delimiter (`\n`, `0x0A`).
///
/// Returns:
///
/// - `Ok(Some(chunk))` - `needle` is found, with chunk ending after needle
/// - `Err(Incomplete)` - `needle` is not found and we're at EOF
/// - `Ok(None)` - `needle` is not found otherwise
#[inline]
pub(crate) fn readline(&mut self) -> Result<Option<Bytes>, Error> {
self.read_until(b"\n")
}
/// Reads bytes until new line delimiter or until EOF.
#[inline]
pub(crate) fn readline_or_eof(&mut self) -> Result<Option<Bytes>, Error> {
match self.readline() {
Err(Error::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())),
line => line,
}
}
/// Puts unprocessed data back to the buffer.
pub(crate) fn unprocessed(&mut self, data: Bytes) {
// TODO: use BytesMut::from when it's released, see https://github.com/tokio-rs/bytes/pull/710
let buf = BytesMut::from(&data[..]);
let buf = mem::replace(&mut self.buf, buf);
self.buf.extend_from_slice(&buf);
}
}
#[cfg(test)]
mod tests {
use actix_http::h1;
use futures_util::future::lazy;
use super::*;
#[actix_rt::test]
async fn basic() {
let (_, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(payload.buf.len(), 0);
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
assert_eq!(None, payload.read_max(1).unwrap());
}
#[actix_rt::test]
async fn eof() {
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_max(4).unwrap());
sender.feed_data(Bytes::from("data"));
sender.feed_eof();
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
assert_eq!(Some(Bytes::from("data")), payload.read_max(4).unwrap());
assert_eq!(payload.buf.len(), 0);
assert!(payload.read_max(1).is_err());
assert!(payload.eof);
}
#[actix_rt::test]
async fn err() {
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));
lazy(|cx| payload.poll_stream(cx)).await.err().unwrap();
}
#[actix_rt::test]
async fn read_max() {
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
sender.feed_data(Bytes::from("line1"));
sender.feed_data(Bytes::from("line2"));
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
assert_eq!(payload.buf.len(), 10);
assert_eq!(Some(Bytes::from("line1")), payload.read_max(5).unwrap());
assert_eq!(payload.buf.len(), 5);
assert_eq!(Some(Bytes::from("line2")), payload.read_max(5).unwrap());
assert_eq!(payload.buf.len(), 0);
}
#[actix_rt::test]
async fn read_exactly() {
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_exact(2));
sender.feed_data(Bytes::from("line1"));
sender.feed_data(Bytes::from("line2"));
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2));
assert_eq!(payload.buf.len(), 8);
assert_eq!(Some(Bytes::from_static(b"ne1l")), payload.read_exact(4));
assert_eq!(payload.buf.len(), 4);
}
#[actix_rt::test]
async fn read_until() {
let (mut sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(None, payload.read_until(b"ne").unwrap());
sender.feed_data(Bytes::from("line1"));
sender.feed_data(Bytes::from("line2"));
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
assert_eq!(
Some(Bytes::from("line")),
payload.read_until(b"ne").unwrap()
);
assert_eq!(payload.buf.len(), 6);
assert_eq!(
Some(Bytes::from("1line2")),
payload.read_until(b"2").unwrap()
);
assert_eq!(payload.buf.len(), 0);
}
}

View File

@ -0,0 +1,60 @@
use std::{cell::Cell, marker::PhantomData, rc::Rc, task};
use local_waker::LocalWaker;
/// Counter. It tracks of number of clones of payloads and give access to payload only to top most.
///
/// - When dropped, parent task is awakened. This is to support the case where `Field` is dropped in
/// a separate task than `Multipart`.
/// - Assumes that parent owners don't move to different tasks; only the top-most is allowed to.
/// - If dropped and is not top most owner, is_clean flag is set to false.
#[derive(Debug)]
pub(crate) struct Safety {
task: LocalWaker,
level: usize,
payload: Rc<PhantomData<bool>>,
clean: Rc<Cell<bool>>,
}
impl Safety {
pub(crate) fn new() -> Safety {
let payload = Rc::new(PhantomData);
Safety {
task: LocalWaker::new(),
level: Rc::strong_count(&payload),
clean: Rc::new(Cell::new(true)),
payload,
}
}
pub(crate) fn current(&self) -> bool {
Rc::strong_count(&self.payload) == self.level && self.clean.get()
}
pub(crate) fn is_clean(&self) -> bool {
self.clean.get()
}
pub(crate) fn clone(&self, cx: &task::Context<'_>) -> Safety {
let payload = Rc::clone(&self.payload);
let s = Safety {
task: LocalWaker::new(),
level: Rc::strong_count(&payload),
clean: self.clean.clone(),
payload,
};
s.task.register(cx.waker());
s
}
}
impl Drop for Safety {
fn drop(&mut self) {
if Rc::strong_count(&self.payload) != self.level {
// Multipart dropped leaving a Field
self.clean.set(false);
}
self.task.wake();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,11 @@
use actix_web::http::header::{self, HeaderMap}; //! Multipart testing utilities.
use bytes::{BufMut as _, Bytes, BytesMut};
use mime::Mime; use actix_web::{
use rand::{ http::header::{self, HeaderMap},
distributions::{Alphanumeric, DistString as _}, web::{BufMut as _, Bytes, BytesMut},
thread_rng,
}; };
use mime::Mime;
use rand::distr::{Alphanumeric, SampleString as _};
const CRLF: &[u8] = b"\r\n"; const CRLF: &[u8] = b"\r\n";
const CRLF_CRLF: &[u8] = b"\r\n\r\n"; const CRLF_CRLF: &[u8] = b"\r\n\r\n";
@ -21,8 +22,7 @@ const BOUNDARY_PREFIX: &str = "------------------------";
/// ///
/// ``` /// ```
/// use actix_multipart::test::create_form_data_payload_and_headers; /// use actix_multipart::test::create_form_data_payload_and_headers;
/// use actix_web::test::TestRequest; /// use actix_web::{test::TestRequest, web::Bytes};
/// use bytes::Bytes;
/// use memchr::memmem::find; /// use memchr::memmem::find;
/// ///
/// let (body, headers) = create_form_data_payload_and_headers( /// let (body, headers) = create_form_data_payload_and_headers(
@ -61,7 +61,7 @@ pub fn create_form_data_payload_and_headers(
content_type: Option<Mime>, content_type: Option<Mime>,
file: Bytes, file: Bytes,
) -> (Bytes, HeaderMap) { ) -> (Bytes, HeaderMap) {
let boundary = Alphanumeric.sample_string(&mut thread_rng(), 32); let boundary = Alphanumeric.sample_string(&mut rand::rng(), 32);
create_form_data_payload_and_headers_with_boundary( create_form_data_payload_and_headers_with_boundary(
&boundary, &boundary,

View File

@ -12,9 +12,11 @@ repository = "https://github.com/actix/actix-web"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[lib] [package.metadata.cargo_check_external_types]
name = "actix_router" allowed_external_types = [
path = "src/lib.rs" "http::*",
"serde::*",
]
[features] [features]
default = ["http", "unicode"] default = ["http", "unicode"]
@ -36,6 +38,9 @@ http = "0.2.7"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
percent-encoding = "2.1" percent-encoding = "2.1"
[lints]
workspace = true
[[bench]] [[bench]]
name = "router" name = "router"
harness = false harness = false

View File

@ -511,11 +511,6 @@ mod tests {
value: String, value: String,
} }
#[derive(Deserialize)]
struct Id {
_id: String,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct Test1(String, u32); struct Test1(String, u32);

View File

@ -1,7 +1,5 @@
//! Resource path matching and router. //! Resource path matching and router.
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]

View File

@ -143,9 +143,9 @@ impl<T: ResourcePath> Path<T> {
for (seg_name, val) in self.segments.iter() { for (seg_name, val) in self.segments.iter() {
if name == seg_name { if name == seg_name {
return match val { return match val {
PathItem::Static(ref s) => Some(s), PathItem::Static(ref seg) => Some(seg),
PathItem::Segment(s, e) => { PathItem::Segment(start, end) => {
Some(&self.path.path()[(*s as usize)..(*e as usize)]) Some(&self.path.path()[(*start as usize)..(*end as usize)])
} }
}; };
} }
@ -193,8 +193,10 @@ impl<'a, T: ResourcePath> Iterator for PathIter<'a, T> {
if self.idx < self.params.segment_count() { if self.idx < self.params.segment_count() {
let idx = self.idx; let idx = self.idx;
let res = match self.params.segments[idx].1 { let res = match self.params.segments[idx].1 {
PathItem::Static(ref s) => s, PathItem::Static(ref seg) => seg,
PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], PathItem::Segment(start, end) => {
&self.params.path.path()[(start as usize)..(end as usize)]
}
}; };
self.idx += 1; self.idx += 1;
return Some((&self.params.segments[idx].0, res)); return Some((&self.params.segments[idx].0, res));
@ -217,8 +219,8 @@ impl<T: ResourcePath> Index<usize> for Path<T> {
fn index(&self, idx: usize) -> &str { fn index(&self, idx: usize) -> &str {
match self.segments[idx].1 { match self.segments[idx].1 {
PathItem::Static(ref s) => s, PathItem::Static(ref seg) => seg,
PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], PathItem::Segment(start, end) => &self.path.path()[(start as usize)..(end as usize)],
} }
} }
} }

View File

@ -105,7 +105,7 @@ fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
let d_low = char::from(d2).to_digit(16)?; let d_low = char::from(d2).to_digit(16)?;
// left shift high nibble by 4 bits // left shift high nibble by 4 bits
Some((d_high as u8) << 4 | (d_low as u8)) Some(((d_high as u8) << 4) | (d_low as u8))
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]

View File

@ -1021,6 +1021,7 @@ impl ResourceDef {
panic!("prefix resource definitions should not have tail segments"); panic!("prefix resource definitions should not have tail segments");
} }
#[allow(clippy::literal_string_with_formatting_args)]
if unprocessed.ends_with('*') { if unprocessed.ends_with('*') {
// unnamed tail segment // unnamed tail segment
@ -1369,6 +1370,7 @@ mod tests {
assert_eq!(path.unprocessed(), ""); assert_eq!(path.unprocessed(), "");
} }
#[allow(clippy::literal_string_with_formatting_args)]
#[test] #[test]
fn newline_patterns_and_paths() { fn newline_patterns_and_paths() {
let re = ResourceDef::new("/user/a\nb"); let re = ResourceDef::new("/user/a\nb");

View File

@ -19,7 +19,7 @@ impl ResourcePath for String {
} }
} }
impl<'a> ResourcePath for &'a str { impl ResourcePath for &str {
fn path(&self) -> &str { fn path(&self) -> &str {
self self
} }

View File

@ -145,6 +145,7 @@ mod tests {
}; };
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
#[allow(clippy::literal_string_with_formatting_args)]
#[test] #[test]
fn test_recognizer_1() { fn test_recognizer_1() {
let mut router = Router::<usize>::build(); let mut router = Router::<usize>::build();

View File

@ -2,6 +2,10 @@
## Unreleased ## Unreleased
## 0.1.5
- Add `TestServerConfig::listen_address()` method.
## 0.1.4 ## 0.1.4
- Add `TestServerConfig::rustls_0_23()` method for Rustls v0.23 support behind new `rustls-0_23` crate feature. - Add `TestServerConfig::rustls_0_23()` method for Rustls v0.23 support behind new `rustls-0_23` crate feature.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.4" version = "0.1.5"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -18,6 +18,22 @@ categories = [
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[package.metadata.cargo_check_external_types]
allowed_external_types = [
"actix_codec::*",
"actix_http_test::*",
"actix_http::*",
"actix_service::*",
"actix_web::*",
"awc::*",
"bytes::*",
"futures_core::*",
"http::*",
"openssl::*",
"rustls::*",
"tokio::*",
]
[features] [features]
default = [] default = []
@ -57,3 +73,6 @@ tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true }
tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true } tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true }
tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true } tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true }
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.24.2", features = ["sync"] }
[lints]
workspace = true

45
actix-test/README.md Normal file
View File

@ -0,0 +1,45 @@
# `actix-test`
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-test?label=latest)](https://crates.io/crates/actix-test)
[![Documentation](https://docs.rs/actix-test/badge.svg?version=0.1.5)](https://docs.rs/actix-test/0.1.5)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-test.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-test/0.1.5/status.svg)](https://deps.rs/crate/actix-test/0.1.5)
[![Download](https://img.shields.io/crates/d/actix-test.svg)](https://crates.io/crates/actix-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end -->
<!-- cargo-rdme start -->
Integration testing tools for Actix Web applications.
The main integration testing tool is [`TestServer`]. It spawns a real HTTP server on an unused port and provides methods that use a real HTTP client. Therefore, it is much closer to real-world cases than using `init_service`, which skips HTTP encoding and decoding.
## Examples
```rust
use actix_web::{get, web, test, App, HttpResponse, Error, Responder};
#[get("/")]
async fn my_handler() -> Result<impl Responder, Error> {
Ok(HttpResponse::Ok())
}
#[actix_rt::test]
async fn test_example() {
let srv = actix_test::start(||
App::new().service(my_handler)
);
let req = srv.get("/");
let res = req.send().await.unwrap();
assert!(res.status().is_success());
}
```
<!-- cargo-rdme end -->

View File

@ -5,6 +5,7 @@
//! real-world cases than using `init_service`, which skips HTTP encoding and decoding. //! real-world cases than using `init_service`, which skips HTTP encoding and decoding.
//! //!
//! # Examples //! # Examples
//!
//! ``` //! ```
//! use actix_web::{get, web, test, App, HttpResponse, Error, Responder}; //! use actix_web::{get, web, test, App, HttpResponse, Error, Responder};
//! //!
@ -26,8 +27,6 @@
//! } //! }
//! ``` //! ```
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
@ -154,7 +153,7 @@ where
// run server in separate orphaned thread // run server in separate orphaned thread
thread::spawn(move || { thread::spawn(move || {
rt::System::new().block_on(async move { rt::System::new().block_on(async move {
let tcp = net::TcpListener::bind(("127.0.0.1", cfg.port)).unwrap(); let tcp = net::TcpListener::bind((cfg.listen_address.clone(), cfg.port)).unwrap();
let local_addr = tcp.local_addr().unwrap(); let local_addr = tcp.local_addr().unwrap();
let factory = factory.clone(); let factory = factory.clone();
let srv_cfg = cfg.clone(); let srv_cfg = cfg.clone();
@ -514,6 +513,7 @@ pub struct TestServerConfig {
tp: HttpVer, tp: HttpVer,
stream: StreamType, stream: StreamType,
client_request_timeout: Duration, client_request_timeout: Duration,
listen_address: String,
port: u16, port: u16,
workers: usize, workers: usize,
disable_redirects: bool, disable_redirects: bool,
@ -532,6 +532,7 @@ impl TestServerConfig {
tp: HttpVer::Both, tp: HttpVer::Both,
stream: StreamType::Tcp, stream: StreamType::Tcp,
client_request_timeout: Duration::from_secs(5), client_request_timeout: Duration::from_secs(5),
listen_address: "127.0.0.1".to_string(),
port: 0, port: 0,
workers: 1, workers: 1,
disable_redirects: false, disable_redirects: false,
@ -607,6 +608,14 @@ impl TestServerConfig {
self self
} }
/// Sets the address the server will listen on.
///
/// By default, only listens on `127.0.0.1`.
pub fn listen_address(mut self, addr: impl Into<String>) -> Self {
self.listen_address = addr.into();
self
}
/// Sets test server port. /// Sets test server port.
/// ///
/// By default, a random free port is determined by the OS. /// By default, a random free port is determined by the OS.
@ -657,9 +666,9 @@ impl TestServer {
let scheme = if self.tls { "https" } else { "http" }; let scheme = if self.tls { "https" } else { "http" };
if uri.starts_with('/') { if uri.starts_with('/') {
format!("{}://localhost:{}{}", scheme, self.addr.port(), uri) format!("{}://{}{}", scheme, self.addr, uri)
} else { } else {
format!("{}://localhost:{}/{}", scheme, self.addr.port(), uri) format!("{}://{}/{}", scheme, self.addr, uri)
} }
} }

View File

@ -2,7 +2,10 @@
## Unreleased ## Unreleased
- Take the encoded buffer when yielding bytes in the response stream rather than splitting the buffer, reducing memory use ## 4.3.1 <!-- v4.3.1+deprecated -->
- Reduce memory usage by `take`-ing (rather than `split`-ing) the encoded buffer when yielding bytes in the response stream.
- Mark crate as deprecated.
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.72.
## 4.3.0 ## 4.3.0

View File

@ -1,17 +1,24 @@
[package] [package]
name = "actix-web-actors" name = "actix-web-actors"
version = "4.3.0" version = "4.3.1+deprecated"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for Actix Web" description = "Actix actors support for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
homepage = "https://actix.rs" homepage.workspace = true
repository = "https://github.com/actix/actix-web" repository.workspace = true
license = "MIT OR Apache-2.0" license.workspace = true
edition = "2021" edition.workspace = true
rust-version.workspace = true
[lib] [package.metadata.cargo_check_external_types]
name = "actix_web_actors" allowed_external_types = [
path = "src/lib.rs" "actix::*",
"actix_http::*",
"actix_web::*",
"bytes::*",
"bytestring::*",
"futures_core::*",
]
[dependencies] [dependencies]
actix = { version = ">=0.12, <0.14", default-features = false } actix = { version = ">=0.12, <0.14", default-features = false }
@ -35,3 +42,6 @@ actix-web = { version = "4", features = ["macros"] }
env_logger = "0.11" env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] } futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
mime = "0.3" mime = "0.3"
[lints]
workspace = true

View File

@ -1,15 +1,17 @@
# `actix-web-actors` # `actix-web-actors`
> Actix actors support for Actix Web. > Actix actors support for Actix Web.
>
> This crate is deprecated. Migrate to [`actix-ws`](https://crates.io/crates/actix-ws).
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.3.0)](https://docs.rs/actix-web-actors/4.3.0) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.3.1)](https://docs.rs/actix-web-actors/4.3.1)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-web-actors.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-actors/4.3.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.3.0) ![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red.svg)
[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -1,5 +1,7 @@
//! Actix actors support for Actix Web. //! Actix actors support for Actix Web.
//! //!
//! This crate is deprecated. Migrate to [`actix-ws`](https://crates.io/crates/actix-ws).
//!
//! # Examples //! # Examples
//! //!
//! ```no_run //! ```no_run
@ -55,8 +57,6 @@
//! * [`HttpContext`]: This struct provides actor support for streaming HTTP responses. //! * [`HttpContext`]: This struct provides actor support for streaming HTTP responses.
//! //!
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]

View File

@ -796,11 +796,8 @@ where
Some(frm) => { Some(frm) => {
let msg = match frm { let msg = match frm {
Frame::Text(data) => { Frame::Text(data) => {
Message::Text(ByteString::try_from(data).map_err(|e| { Message::Text(ByteString::try_from(data).map_err(|err| {
ProtocolError::Io(io::Error::new( ProtocolError::Io(io::Error::new(io::ErrorKind::Other, err))
io::ErrorKind::Other,
format!("{}", e),
))
})?) })?)
} }
Frame::Binary(data) => Message::Binary(data), Frame::Binary(data) => Message::Binary(data),

View File

@ -34,4 +34,7 @@ actix-web = "4"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
trybuild = "1" trybuild = "1"
rustversion = "1" rustversion-msrv = "0.100"
[lints]
workspace = true

View File

@ -73,8 +73,6 @@
//! [DELETE]: macro@delete //! [DELETE]: macro@delete
#![recursion_limit = "512"] #![recursion_limit = "512"]
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]

View File

@ -136,7 +136,7 @@ async fn routes_overlapping_inaccessible_test(req: HttpRequest) -> impl Responde
} }
#[get("/custom_resource_name", name = "custom")] #[get("/custom_resource_name", name = "custom")]
async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder { async fn custom_resource_name_test(req: HttpRequest) -> impl Responder {
assert!(req.url_for_static("custom").is_ok()); assert!(req.url_for_static("custom").is_ok());
assert!(req.url_for_static("custom_resource_name_test").is_err()); assert!(req.url_for_static("custom_resource_name_test").is_err());
HttpResponse::Ok() HttpResponse::Ok()
@ -145,7 +145,7 @@ async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder {
mod guard_module { mod guard_module {
use actix_web::{guard::GuardContext, http::header}; use actix_web::{guard::GuardContext, http::header};
pub fn guard(ctx: &GuardContext) -> bool { pub fn guard(ctx: &GuardContext<'_>) -> bool {
ctx.header::<header::Accept>() ctx.header::<header::Accept>()
.map(|h| h.preference() == "image/*") .map(|h| h.preference() == "image/*")
.unwrap_or(false) .unwrap_or(false)

View File

@ -1,7 +1,7 @@
use actix_web::{guard::GuardContext, http, http::header, web, App, HttpResponse, Responder}; use actix_web::{guard::GuardContext, http, http::header, web, App, HttpResponse, Responder};
use actix_web_codegen::{delete, get, post, route, routes, scope}; use actix_web_codegen::{delete, get, post, route, routes, scope};
pub fn image_guard(ctx: &GuardContext) -> bool { pub fn image_guard(ctx: &GuardContext<'_>) -> bool {
ctx.header::<header::Accept>() ctx.header::<header::Accept>()
.map(|h| h.preference() == "image/*") .map(|h| h.preference() == "image/*")
.unwrap_or(false) .unwrap_or(false)

View File

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

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