Compare commits

...

319 Commits

Author SHA1 Message Date
Armand Mégrot 26195c8080
Merge branch 'main' into master 2025-12-08 14:13:32 +01:00
dependabot[bot] 28843c4ed3
build(deps): bump tracing from 0.1.42 to 0.1.43 (#3846)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 09:56:58 +00:00
dependabot[bot] f192357f09
build(deps): bump time from 0.3.41 to 0.3.44 (#3848)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 09:56:50 +00:00
dependabot[bot] d9c676715b
build(deps): bump taiki-e/install-action from 2.62.56 to 2.62.60 (#3849)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 09:56:38 +00:00
dependabot[bot] 16eba96674
build(deps): bump EmbarkStudios/cargo-deny-action from 2.0.13 to 2.0.14 (#3847)
Bumps [EmbarkStudios/cargo-deny-action](https://github.com/embarkstudios/cargo-deny-action) from 2.0.13 to 2.0.14.
- [Release notes](https://github.com/embarkstudios/cargo-deny-action/releases)
- [Commits](f2ba7abc2a...76cd80eb77)

---
updated-dependencies:
- dependency-name: EmbarkStudios/cargo-deny-action
  dependency-version: 2.0.14
  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-12-01 04:24:56 +00:00
dependabot[bot] c7710036de
build(deps): bump divan from 0.1.15 to 0.1.21 (#3845)
Bumps [divan](https://github.com/nvzqz/divan) from 0.1.15 to 0.1.21.
- [Changelog](https://github.com/nvzqz/divan/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nvzqz/divan/compare/v0.1.15...v0.1.21)

---
updated-dependencies:
- dependency-name: divan
  dependency-version: 0.1.21
  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-12-01 04:20:13 +00:00
Rob Ede ff50dd03a6
chore: upgrade MSRV to 1.82 (#3844) 2025-11-26 20:26:35 +00:00
Yuki Okushi 24489d4c6d
doc(web): Fix links in README.md (#3834) 2025-11-26 16:40:07 +00:00
Rob Ede 30b82062a1
chore(actix-files): prepare release 0.6.9 2025-11-26 16:37:53 +00:00
Rob Ede fe6c0a3b2b
chore(actix-web): prepare release 4.12.1 2025-11-26 16:34:38 +00:00
Rob Ede 37a8b9caa8
chore(web): fix actix-http requirement
fixes #3832
2025-11-26 16:32:41 +00:00
Rob Ede 37ff707b52
chore: fix derive_more feature requirements for actix-files
fixes #3842
2025-11-26 16:32:38 +00:00
dependabot[bot] bbfd402f09
build(deps): bump bytes from 1.10.1 to 1.11.0 (#3839)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.1 to 1.11.0.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.1...v1.11.0)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.0
  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-11-24 12:17:45 +00:00
dependabot[bot] ce258a00c9
build(deps): bump taiki-e/install-action from 2.62.51 to 2.62.56 (#3837)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.51 to 2.62.56.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](0be4756f42...f79fe7514d)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.56
  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-11-24 05:59:43 +00:00
dependabot[bot] b2205d80f6
build(deps): bump actions/checkout from 5.0.0 to 6.0.0 (#3836)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](08c6903cd8...1af3b93b68)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.0
  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>
2025-11-24 05:59:26 +00:00
dependabot[bot] fe890a73b2
build(deps): bump taiki-e/install-action from 2.62.49 to 2.62.51 (#3831)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 10:51:05 +00:00
Yuki Okushi d119500f93
release: actix-web v4.12.0 (#3830) 2025-11-16 15:52:36 +09:00
Yuki Okushi a3f95ee1ef
feat: improve `HttpResponseBuilder::streaming` with SizedStream (#3829) 2025-11-16 15:22:29 +09:00
Ruchir e1da110e92
chore: Add public export for `EitherExtractError` (#3826)
* chore: Export EitherExtractError for public use

* refactor: export EitherExtractError
2025-11-11 15:28:40 +00:00
dependabot[bot] 219b988300
build(deps): bump taiki-e/install-action from 2.62.46 to 2.62.49 (#3823)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.46 to 2.62.49.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](f535147c22...44c6d64aa6)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.49
  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-11-10 01:54:15 +00:00
dependabot[bot] 9cf3521390
build(deps): bump quote from 1.0.41 to 1.0.42 (#3825)
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.41 to 1.0.42.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.41...1.0.42)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.42
  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-11-10 01:53:48 +00:00
dependabot[bot] 8b103199ea
build(deps): bump taiki-e/install-action from 2.62.45 to 2.62.46 (#3820)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.45 to 2.62.46.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](81ee1d48d9...f535147c22)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.46
  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-11-05 11:22:09 +00:00
Yuki Okushi d5fae3ecc2
chore: rename branch to main (#3821) 2025-11-05 20:13:50 +09:00
dependabot[bot] e6958cc677
build(deps): bump taiki-e/install-action from 2.62.38 to 2.62.45 (#3819)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.38 to 2.62.45.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](c5b1b6f479...81ee1d48d9)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.45
  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-11-03 00:41:19 +00:00
dependabot[bot] 3dd42ee13e
build(deps): bump tokio-util from 0.7.16 to 0.7.17 (#3817)
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.16 to 0.7.17.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.16...tokio-util-0.7.17)

---
updated-dependencies:
- dependency-name: tokio-util
  dependency-version: 0.7.17
  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-11-03 00:41:06 +00:00
Yuki Okushi 627af8ce57
chore(gha): setup cargo-deny (#3816) 2025-11-02 10:52:25 +09:00
dependabot[bot] 97e701721b
build(deps): bump flate2 from 1.1.4 to 1.1.5 (#3810)
Bumps [flate2](https://github.com/rust-lang/flate2-rs) from 1.1.4 to 1.1.5.
- [Release notes](https://github.com/rust-lang/flate2-rs/releases)
- [Commits](https://github.com/rust-lang/flate2-rs/compare/1.1.4...1.1.5)

---
updated-dependencies:
- dependency-name: flate2
  dependency-version: 1.1.5
  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>
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2025-10-27 11:43:57 +00:00
dependabot[bot] 9e1118cfdd
build(deps): bump proc-macro2 from 1.0.101 to 1.0.103 (#3809)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.101 to 1.0.103.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.101...1.0.103)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-version: 1.0.103
  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-10-27 11:19:03 +00:00
dependabot[bot] a769f6636c
build(deps): bump taiki-e/install-action from 2.62.33 to 2.62.38 (#3808)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.33 to 2.62.38.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](e43a5023a7...c5b1b6f479)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.38
  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-10-27 11:18:40 +00:00
Yuki Okushi 2967e4af9f
test: disable docsrs test for now (#3811) 2025-10-27 20:17:32 +09:00
dependabot[bot] 832f893bc7
build(deps): bump tokio from 1.47.1 to 1.48.0 (#3805)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.47.1 to 1.48.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.47.1...tokio-1.48.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.48.0
  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-10-22 09:03:56 +00:00
dependabot[bot] 55ebece1fa
build(deps): bump socket2 from 0.6.0 to 0.6.1 (#3807)
Bumps [socket2](https://github.com/rust-lang/socket2) from 0.6.0 to 0.6.1.
- [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/compare/v0.6.0...v0.6.1)

---
updated-dependencies:
- dependency-name: socket2
  dependency-version: 0.6.1
  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-10-20 13:03:06 +00:00
dependabot[bot] 6ecfcb6418
build(deps): bump cfg-if from 1.0.3 to 1.0.4 (#3804)
Bumps [cfg-if](https://github.com/rust-lang/cfg-if) from 1.0.3 to 1.0.4.
- [Release notes](https://github.com/rust-lang/cfg-if/releases)
- [Changelog](https://github.com/rust-lang/cfg-if/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cfg-if/compare/v1.0.3...v1.0.4)

---
updated-dependencies:
- dependency-name: cfg-if
  dependency-version: 1.0.4
  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-10-20 13:01:35 +00:00
dependabot[bot] 856263cfd2
build(deps): bump bitflags from 2.9.4 to 2.10.0 (#3803)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.9.4 to 2.10.0.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.9.4...2.10.0)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-version: 2.10.0
  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-10-20 13:01:21 +00:00
dependabot[bot] 6d9e47720f
build(deps): bump taiki-e/install-action from 2.62.28 to 2.62.33 (#3802)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.28 to 2.62.33.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](e7ef886cf8...e43a5023a7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.33
  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-10-20 13:01:01 +00:00
dependabot[bot] 35cc89a5a8
build(deps): bump taiki-e/install-action from 2.62.20 to 2.62.28 (#3800)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.20 to 2.62.28.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](f355b1dcaf...e7ef886cf8)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.28
  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-10-13 08:33:51 +00:00
dependabot[bot] c8c00137b0
build(deps): bump regex-lite from 0.1.7 to 0.1.8 (#3801)
Bumps [regex-lite](https://github.com/rust-lang/regex) from 0.1.7 to 0.1.8.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/regex-lite-0.1.7...regex-lite-0.1.8)

---
updated-dependencies:
- dependency-name: regex-lite
  dependency-version: 0.1.8
  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-10-13 08:33:35 +00:00
dependabot[bot] e8f0e8b1c4
build(deps): bump quote from 1.0.40 to 1.0.41 (#3794)
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.40 to 1.0.41.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.40...1.0.41)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.41
  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-10-06 09:19:09 +00:00
dependabot[bot] df016be17c
build(deps): bump taiki-e/install-action from 2.62.11 to 2.62.20 (#3792)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.11 to 2.62.20.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](efd8b64311...f355b1dcaf)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.20
  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-10-06 09:18:53 +00:00
dependabot[bot] d36a5b49d8
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.15.1 to 1.15.2 (#3793)
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.15.1 to 1.15.2.
- [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](02be93da58...1780873c7b)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.15.2
  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-10-06 09:18:21 +00:00
dependabot[bot] 832a1da262
build(deps): bump flate2 from 1.1.2 to 1.1.4 (#3795)
Bumps [flate2](https://github.com/rust-lang/flate2-rs) from 1.1.2 to 1.1.4.
- [Release notes](https://github.com/rust-lang/flate2-rs/releases)
- [Commits](https://github.com/rust-lang/flate2-rs/compare/1.1.2...1.1.4)

---
updated-dependencies:
- dependency-name: flate2
  dependency-version: 1.1.4
  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-10-06 09:17:54 +00:00
Yuki Okushi 25963dbdf1
Prepare release for actix-http, awc (#3791) 2025-10-05 11:30:27 +09:00
asonix 4df9953c86
actix-http: h1: stop pipelining when not reading full requests (#3721)
* actix-http: h1: stop pipelining when not reading full requests

The existing pipelining behavior of the h1 dispatcher can cause client timeouts
if the entire request body isn't read. It puts the dispatcher into a state where
it refuses to read more (payload dropped) but there are still bytes in the buffer
from the request body.

This solution adds the SHUTDOWN flag in addition to the FINISHED flag
when completing a response when both the following are true:

1. There are no messages in `this.messages`
2. There is still a payload in `this.payload`

This combination implies two things. First, that we have not parsed a
pipelined request after the request we have just responded to. Second,
that the current request payload has not been fed an EOF. Because there
are no pipelined requests, we know that the current request payload
belongs to the request we have just responded to, and because the
request payload has not been fed an EOF, we know we never finished
reading it.

When this occurs, adding the SHUTDOWN flag to the dispatcher triggers a
`flush` and a `poll_shutdown` on the IO resource on the next poll.

* Remove printlns from dispatcher

* Add test that fails without changes & passes with changes

* Add changelog entry for h1 shutdown

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2025-10-05 01:07:35 +00:00
Kevin Logan 016841137e
fix(awc): close connections after GO_AWAY local or remote GO_AWAY errors (#3790) 2025-10-05 00:44:02 +00:00
Thales Fragoso fde7934243
Add option to allow/disallow half closures in HTTP/1 (#3777)
* Add ServiceConfigBuilder to facilitate future extensions

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

* Add option to allow/disallow h1 half closures

The default is set to allow, thus reverting a change made in 3.11.1

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

* Add tests for h1 half closures

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

* Comply with clippy

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

* Adjust imports to satisfy fmt

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

---------

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>
2025-10-04 23:03:26 +00:00
Yuki Okushi 429ff82a4b
fix: rename doc_auto_cfg feature (#3787) 2025-09-29 10:42:56 +00:00
dependabot[bot] 28a3a44e52
build(deps): bump tempfile from 3.22.0 to 3.23.0 (#3785)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.22.0 to 3.23.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.22.0...v3.23.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.23.0
  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-09-29 07:44:59 +00:00
dependabot[bot] 91b62b950d
build(deps): bump taiki-e/cache-cargo-install-action from 2.3.0 to 2.3.1 (#3782)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 2.3.0 to 2.3.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](b33c63d3b3...7447f04c51)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-version: 2.3.1
  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-09-29 01:26:26 +00:00
dependabot[bot] 8935f7a4e3
build(deps): bump serde from 1.0.226 to 1.0.228 (#3786)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.226 to 1.0.228.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.226...v1.0.228)

---
updated-dependencies:
- dependency-name: serde
  dependency-version: 1.0.228
  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-09-29 01:26:15 +00:00
dependabot[bot] df2c6b3208
build(deps): bump taiki-e/install-action from 2.62.0 to 2.62.11 (#3781)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.0 to 2.62.11.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](0e09747a63...efd8b64311)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.11
  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-09-29 00:31:04 +00:00
dependabot[bot] d580cf0709
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.15.0 to 1.15.1 (#3784)
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.15.0 to 1.15.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](2fcdc490d6...02be93da58)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.15.1
  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-09-29 00:30:53 +00:00
dependabot[bot] 0f0ceb5a73
build(deps): bump memchr from 2.7.5 to 2.7.6 (#3783)
Bumps [memchr](https://github.com/BurntSushi/memchr) from 2.7.5 to 2.7.6.
- [Commits](https://github.com/BurntSushi/memchr/compare/2.7.5...2.7.6)

---
updated-dependencies:
- dependency-name: memchr
  dependency-version: 2.7.6
  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-09-29 00:30:40 +00:00
dependabot[bot] 5c6a29f435
build(deps): bump time from 0.3.43 to 0.3.44 (#3773)
Bumps [time](https://github.com/time-rs/time) from 0.3.43 to 0.3.44.
- [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.43...v0.3.44)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.44
  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-09-22 11:06:13 +00:00
Martin Grigorov b200a45c8e
Remove duplicated entry for "serde::*" in cargo_check_external_types config (#3776) 2025-09-22 11:05:48 +00:00
dependabot[bot] 89fea1f5ee
build(deps): bump taiki-e/install-action from 2.61.3 to 2.62.0 (#3774)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.61.3 to 2.62.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](67cc679904...0e09747a63)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.62.0
  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-09-22 11:04:42 +00:00
dependabot[bot] 5d0cf52294
build(deps): bump serde from 1.0.223 to 1.0.226 (#3775)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.223 to 1.0.226.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.223...v1.0.226)

---
updated-dependencies:
- dependency-name: serde
  dependency-version: 1.0.226
  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-09-22 11:04:36 +00:00
Rob Ede 41d0176c1d
chore(actix-files): prepare release 0.6.8 2025-09-15 09:53:32 +01:00
francesco-gaglione 6e6c2491c6
Add support for permanent redirects in `Files` service (#3767)
* Add support for permanent redirects in `Files` service

* Rename `with_permanent_redirect_directory` to `with_permanent_redirect` in `Files` service

* Tweak doc comment

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2025-09-15 08:33:33 +00:00
dependabot[bot] a06cdf3754
build(deps): bump tempfile from 3.21.0 to 3.22.0 (#3771)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.21.0 to 3.22.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.21.0...v3.22.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.22.0
  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-09-15 02:01:21 +00:00
dependabot[bot] 25a12177af
build(deps): bump serde from 1.0.219 to 1.0.223 (#3772)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.219 to 1.0.223.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.219...v1.0.223)

---
updated-dependencies:
- dependency-name: serde
  dependency-version: 1.0.223
  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-09-15 02:01:04 +00:00
dependabot[bot] f3ce1bc45f
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.14.1 to 1.15.0 (#3768)
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.14.1 to 1.15.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](ac90e63697...2fcdc490d6)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.15.0
  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-09-15 01:08:20 +00:00
dependabot[bot] 7f02a7e41a
build(deps): bump trybuild from 1.0.110 to 1.0.111 (#3770)
Bumps [trybuild](https://github.com/dtolnay/trybuild) from 1.0.110 to 1.0.111.
- [Release notes](https://github.com/dtolnay/trybuild/releases)
- [Commits](https://github.com/dtolnay/trybuild/compare/1.0.110...1.0.111)

---
updated-dependencies:
- dependency-name: trybuild
  dependency-version: 1.0.111
  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-09-15 01:03:00 +00:00
dependabot[bot] a619022c95
build(deps): bump taiki-e/install-action from 2.59.1 to 2.61.3 (#3769)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.59.1 to 2.61.3.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](57511bcdf8...67cc679904)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.61.3
  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-09-15 00:57:15 +00:00
Rob Ede 1d6e978d82
chore(awc): prepare release 3.8.0 2025-09-09 09:03:53 +01:00
Rob Ede d43738306c
refactor: init hickory DNS using OnceCell 2025-09-09 09:03:09 +01:00
Joel Wurtz 62d6d5863e
feat(awc): replace trust dns by hickory dns (#3528)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-09-09 08:38:34 +01:00
dependabot[bot] f4ee39cdd7
build(deps): bump log from 0.4.27 to 0.4.28 (#3759)
Bumps [log](https://github.com/rust-lang/log) from 0.4.27 to 0.4.28.
- [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.27...0.4.28)

---
updated-dependencies:
- dependency-name: log
  dependency-version: 0.4.28
  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-09-08 00:40:08 +00:00
dependabot[bot] f54254d0bd
build(deps): bump bitflags from 2.9.3 to 2.9.4 (#3760)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.9.3 to 2.9.4.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.9.3...2.9.4)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-version: 2.9.4
  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-09-08 00:32:48 +00:00
dependabot[bot] 78ee652140
build(deps): bump codecov/codecov-action from 5.5.0 to 5.5.1 (#3761)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.0 to 5.5.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](fdcc847654...5a1091511a)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.5.1
  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-09-08 00:32:37 +00:00
dependabot[bot] 03e3e344cc
build(deps): bump taiki-e/install-action from 2.58.29 to 2.59.1 (#3762)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.58.29 to 2.59.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](14083e64ac...57511bcdf8)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.59.1
  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-09-08 00:32:26 +00:00
dependabot[bot] d1acaa3b3a
build(deps): bump time from 0.3.41 to 0.3.42 (#3755)
Bumps [time](https://github.com/time-rs/time) from 0.3.41 to 0.3.42.
- [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.41...v0.3.42)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.42
  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-09-06 21:13:52 +00:00
Rob Ede 4febfa4446
ci: pin time crate for msrv 2025-09-06 21:31:35 +01:00
dependabot[bot] ae354b8edc
build(deps): bump taiki-e/install-action from 2.58.21 to 2.58.29 (#3753)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.58.21 to 2.58.29.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](f63c33fd96...14083e64ac)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.58.29
  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-09-01 12:00:08 +00:00
dependabot[bot] 7726669f9f
build(deps): bump actix-rt from 2.10.0 to 2.11.0 (#3754)
Bumps [actix-rt](https://github.com/actix/actix-net) from 2.10.0 to 2.11.0.
- [Release notes](https://github.com/actix/actix-net/releases)
- [Commits](https://github.com/actix/actix-net/compare/rt-v2.10.0...rt-v2.11.0)

---
updated-dependencies:
- dependency-name: actix-rt
  dependency-version: 2.11.0
  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-09-01 12:00:01 +00:00
dependabot[bot] 65c5a043d7
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.14.0 to 1.14.1 (#3751)
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.14.0 to 1.14.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](ab6845274e...ac90e63697)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.14.1
  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-09-01 11:59:50 +00:00
dependabot[bot] c85e058f5d
build(deps): bump rui314/setup-mold from 7344740a9418dcdcb481c7df83d9fbd1d5072d7d to 725a8794d15fc7563f59595bd9556495c0564878 (#3752)
build(deps): bump rui314/setup-mold

Bumps [rui314/setup-mold](https://github.com/rui314/setup-mold) from 7344740a9418dcdcb481c7df83d9fbd1d5072d7d to 725a8794d15fc7563f59595bd9556495c0564878.
- [Commits](7344740a94...725a8794d1)

---
updated-dependencies:
- dependency-name: rui314/setup-mold
  dependency-version: 725a8794d15fc7563f59595bd9556495c0564878
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-01 01:54:18 +00:00
Rob Ede 98d7d0b46b
chore(actix-files): prepare release 0.6.7 2025-08-29 22:31:48 +01:00
Rob Ede 4966a54e05
refactor(files): rename read_mode_threshold fn 2025-08-29 22:30:47 +01:00
Andrew Scott 00b0f8f700
feat(actix-files): opt-in filesize threshold for faster synchronous reads (#3706)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-08-29 21:52:34 +01:00
励志买套上海苏河湾大平层 3c2907da41
docs(middleware): complete middleware author's guide (#3680)
Add comprehensive documentation for middleware development in Actix Web, including:
- Detailed explanation of middleware concepts and execution flow
- Clear description of middleware traits and their responsibilities
- Guidelines for body type handling
- Best practices for middleware development
- Error handling recommendations
- Usage scenarios and anti-patterns

Co-authored-by: chenjjiaa <chenjjiaaa@gmail.com>
2025-08-29 20:12:05 +00:00
George Pollard 5041cd1c65
Make 'ws' feature of actix-http optional in actix-web (#3734)
* Make 'ws' feature of actix-http optional

* Update CHANGES.md

* Update actix-web-actors

* Update CHANGES.md

* nits

* nits

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-08-29 02:50:05 +00:00
Thales d3c46537b3
fix(http): Wake Payload when feeding error or EOF (#3749)
* fix(http): Add failing tests to demonstrate the payload problem

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

* fix(http): Wake Payload when feeding error or eof

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

---------

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>
2025-08-29 02:47:03 +00:00
Rob Ede 8996198f2c
chore: require h2 versions after MadeYouReset fix 2025-08-26 23:59:57 +01:00
Rob Ede 68624ec63b
chore: remove now-useless docs.rs flags 2025-08-26 23:51:22 +01:00
Rob Ede bcd0ffb016
chore: add multi-crate publish script 2025-08-26 09:25:22 +01:00
Rob Ede 9fb6c13a1a
ci: fix msrv job 2025-08-26 08:26:49 +01:00
Rob Ede 05cfef7f4b
ci: fix msrv job 2025-08-26 08:18:34 +01:00
Rob Ede 8f3eb32a32
chore: fix justfile for msrv 2025-08-26 08:00:19 +01:00
Rob Ede ddd16ec9db
chore(actix-http): prepare release 3.11.1 2025-08-26 07:28:27 +01:00
dependabot[bot] 9c47a247fb
build(deps): bump bitflags from 2.9.2 to 2.9.3 (#3745)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.9.2 to 2.9.3.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.9.2...2.9.3)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-version: 2.9.3
  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-08-25 12:31:30 +00:00
dependabot[bot] 2536823e3b
build(deps): bump regex-lite from 0.1.6 to 0.1.7 (#3743)
Bumps [regex-lite](https://github.com/rust-lang/regex) from 0.1.6 to 0.1.7.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/regex-lite-0.1.6...regex-lite-0.1.7)

---
updated-dependencies:
- dependency-name: regex-lite
  dependency-version: 0.1.7
  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-08-25 10:43:50 +00:00
dependabot[bot] e3f81d0643
build(deps): bump url from 2.5.4 to 2.5.7 (#3742)
Bumps [url](https://github.com/servo/rust-url) from 2.5.4 to 2.5.7.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/commits)

---
updated-dependencies:
- dependency-name: url
  dependency-version: 2.5.7
  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-08-25 10:42:00 +00:00
dependabot[bot] a84aee836b
build(deps): bump tempfile from 3.20.0 to 3.21.0 (#3744)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.20.0 to 3.21.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/commits)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.21.0
  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-08-25 10:40:55 +00:00
dependabot[bot] 1f08cb24c3
build(deps): bump regex from 1.11.1 to 1.11.2 (#3746)
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.1 to 1.11.2.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.1...1.11.2)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.11.2
  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-08-25 10:36:40 +00:00
dependabot[bot] 1b49047086
build(deps): bump codecov/codecov-action from 5.4.3 to 5.5.0 (#3741)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.3 to 5.5.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](18283e04ce...fdcc847654)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.5.0
  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-08-25 10:33:15 +00:00
dependabot[bot] 9fe033a963
build(deps): bump taiki-e/install-action from 2.58.17 to 2.58.21 (#3740)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.58.17 to 2.58.21.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](ad95d4e02e...f63c33fd96)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.58.21
  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-08-25 10:32:36 +00:00
dependabot[bot] 2ba69a1904
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.13.0 to 1.14.0 (#3739)
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.13.0 to 1.14.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](fb51252c7b...ab6845274e)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.14.0
  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-08-25 10:31:31 +00:00
Andrew Scott c6352005f7
fix: include content-length with bytes payload (#3695)
* fix: include content-length with bytes payload

* chore: json unit-test patch

* Update doc comment

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2025-08-24 09:42:11 +00:00
Yuki Okushi aeb42486af
fix(actix-web): improve streaming response behavior for Content-Length and Content-Type (#3737)
- Set default Content-Type to application/octet-stream for streaming responses
- Respect Content-Length header by automatically calling `no_chunking()` when set
- Resolves issue where Content-Length was ignored in streaming responses

Fixes #2306

---------

Co-authored-by: Yinuo Deng <i@dyn.im>
2025-08-23 12:23:57 +09:00
yanyuxing 30f7df5853
fix(actix-http): wake before return pending when read half disconnect and need to shutdown (#3665) 2025-08-23 00:56:39 +00:00
Yuki Okushi e3ae20de30
chore: Pin actions to full commit SHA (#3736) 2025-08-22 16:41:52 +09:00
Guido Vranken 5f6c84494a
Fix integer overflow in actix_http::ws::Parser::parse (#3728)
* Fix integer overflow in actix_http::ws::Parser::parse

* Add changelog

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2025-08-22 06:24:22 +00:00
宮水_五葉 37203c7fd8
fix: increase total limit in multipart example (#3567)
* fix: increase total limit in multipart example

* update readme

* Clarify where the limit comes from

* Fix link

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2025-08-22 05:38:51 +00:00
dependabot[bot] 971bbc1468
build(deps): bump rustversion from 1.0.21 to 1.0.22 (#3726)
Bumps [rustversion](https://github.com/dtolnay/rustversion) from 1.0.21 to 1.0.22.
- [Release notes](https://github.com/dtolnay/rustversion/releases)
- [Commits](https://github.com/dtolnay/rustversion/compare/1.0.21...1.0.22)

---
updated-dependencies:
- dependency-name: rustversion
  dependency-version: 1.0.22
  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-08-22 02:53:12 +00:00
dependabot[bot] d08a85b470
build(deps): bump slab from 0.4.10 to 0.4.11 (#3725)
Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/tokio-rs/slab/releases)
- [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/slab/compare/v0.4.10...v0.4.11)

---
updated-dependencies:
- dependency-name: slab
  dependency-version: 0.4.11
  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-08-22 02:38:10 +00:00
dependabot[bot] d94442246f
build(deps): bump proc-macro2 from 1.0.95 to 1.0.101 (#3731)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.95 to 1.0.101.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.95...1.0.101)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-version: 1.0.101
  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-08-22 02:37:50 +00:00
dependabot[bot] 706636da67
build(deps): bump taiki-e/cache-cargo-install-action from 2.2.0 to 2.3.0 (#3700)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 2.2.0 to 2.3.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.2.0...v2.3.0)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-version: 2.3.0
  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-08-22 02:04:35 +00:00
dependabot[bot] be6be9fbcc
build(deps): bump bitflags from 2.9.1 to 2.9.2 (#3732)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.9.1 to 2.9.2.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.9.1...2.9.2)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-version: 2.9.2
  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-08-22 02:03:53 +00:00
dependabot[bot] cd71768509
build(deps): bump brotli from 8.0.1 to 8.0.2 (#3733)
Bumps [brotli](https://github.com/dropbox/rust-brotli) from 8.0.1 to 8.0.2.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits/8.0.2)

---
updated-dependencies:
- dependency-name: brotli
  dependency-version: 8.0.2
  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-08-22 01:44:36 +00:00
Yuki Okushi d5abb5ae45
docs(web): Fix invalid link for `HttpRequest::match_info()` (#3735) 2025-08-20 12:58:28 +00:00
dependabot[bot] 522f60fa8c
build(deps): bump taiki-e/install-action from 2.57.5 to 2.58.17 (#3729)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 01:43:09 +01:00
dependabot[bot] c65633b548
build(deps): bump actions/checkout from 4 to 5 (#3730) 2025-08-18 01:33:06 +01:00
dependabot[bot] 351f7a17ed
build(deps): bump taiki-e/install-action from 2.56.19 to 2.57.5 (#3712)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.56.19 to 2.57.5.
- [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.56.19...v2.57.5)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.57.5
  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-08-05 22:02:35 +00:00
dependabot[bot] 254fcab19d
build(deps): bump tokio from 1.47.0 to 1.47.1 (#3711)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.47.0 to 1.47.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.47.0...tokio-1.47.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.47.1
  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-08-05 22:02:32 +00:00
dependabot[bot] 3c67d04cbe
build(deps): bump serde_json from 1.0.141 to 1.0.142 (#3710)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.141 to 1.0.142.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.141...v1.0.142)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.142
  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-08-05 22:02:31 +00:00
dependabot[bot] b355603c99
build(deps): bump trybuild from 1.0.106 to 1.0.110 (#3709)
Bumps [trybuild](https://github.com/dtolnay/trybuild) from 1.0.106 to 1.0.110.
- [Release notes](https://github.com/dtolnay/trybuild/releases)
- [Commits](https://github.com/dtolnay/trybuild/compare/1.0.106...1.0.110)

---
updated-dependencies:
- dependency-name: trybuild
  dependency-version: 1.0.110
  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-08-05 22:02:29 +00:00
dependabot[bot] a81c05818f
build(deps): bump tokio-util from 0.7.15 to 0.7.16 (#3708)
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.15 to 0.7.16.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.15...tokio-util-0.7.16)

---
updated-dependencies:
- dependency-name: tokio-util
  dependency-version: 0.7.16
  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-08-05 22:02:28 +00:00
dependabot[bot] aceff9d13f
build(deps): bump tokio from 1.46.1 to 1.47.0 (#3702)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.46.1 to 1.47.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.46.1...tokio-1.47.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.47.0
  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-07-30 01:32:39 +00:00
dependabot[bot] 1896ff2ec7
build(deps): bump rand from 0.9.1 to 0.9.2 (#3697)
Bumps [rand](https://github.com/rust-random/rand) from 0.9.1 to 0.9.2.
- [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/rand_core-0.9.1...rand_core-0.9.2)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.9.2
  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-07-27 05:44:57 +00:00
dependabot[bot] 0eef04ed0a
build(deps): bump serde_json from 1.0.140 to 1.0.141 (#3698)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.140 to 1.0.141.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.140...v1.0.141)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.141
  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-07-27 02:59:17 +00:00
dependabot[bot] 6e396bc352
build(deps): bump taiki-e/install-action from 2.56.13 to 2.56.19 (#3696)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.56.13 to 2.56.19.
- [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.56.13...v2.56.19)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.56.19
  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-07-27 02:45:38 +00:00
dependabot[bot] b6ad483699
build(deps): bump tokio from 1.45.1 to 1.46.1 (#3689)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.45.1 to 1.46.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.45.1...tokio-1.46.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.46.1
  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-07-14 01:52:41 +00:00
dependabot[bot] c002fd783c
build(deps): bump taiki-e/install-action from 2.54.3 to 2.56.13 (#3693)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.54.3 to 2.56.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.54.3...v2.56.13)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.56.13
  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-07-14 01:16:19 +00:00
Thomas de Zeeuw 95ad1caa23
Update socket2 to v0.6 (#3688) 2025-07-14 01:13:01 +00:00
dependabot[bot] eb906d077a
build(deps): bump trybuild from 1.0.105 to 1.0.106 (#3692)
Bumps [trybuild](https://github.com/dtolnay/trybuild) from 1.0.105 to 1.0.106.
- [Release notes](https://github.com/dtolnay/trybuild/releases)
- [Commits](https://github.com/dtolnay/trybuild/compare/1.0.105...1.0.106)

---
updated-dependencies:
- dependency-name: trybuild
  dependency-version: 1.0.106
  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-07-14 01:01:17 +00:00
dependabot[bot] f08fa6b684
build(deps): bump h2 from 0.3.26 to 0.3.27 (#3691)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.26 to 0.3.27.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.27/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.26...v0.3.27)

---
updated-dependencies:
- dependency-name: h2
  dependency-version: 0.3.27
  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-07-14 00:50:00 +00:00
dependabot[bot] 3f9d88f859
build(deps): bump taiki-e/install-action from 2.54.0 to 2.54.3 (#3686)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.54.0 to 2.54.3.
- [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.54.0...v2.54.3)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.54.3
  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-06-30 22:59:38 +00:00
dependabot[bot] 2eb801cb59
build(deps): bump taiki-e/cache-cargo-install-action from 2.1.2 to 2.2.0 (#3687)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 2.1.2 to 2.2.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.1.2...v2.2.0)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-version: 2.2.0
  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-06-30 22:58:36 +00:00
dependabot[bot] 43d3c6af83
build(deps): bump taiki-e/cache-cargo-install-action from 2.1.1 to 2.1.2 (#3682)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 2.1.1 to 2.1.2.
- [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.1...v2.1.2)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-version: 2.1.2
  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-06-28 13:38:21 +00:00
dependabot[bot] fdde0b8b14
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.12.0 to 1.13.0 (#3683)
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.12.0 to 1.13.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.12.0...v1.13.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.13.0
  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-06-23 11:18:19 +00:00
dependabot[bot] 2a634a8af2
build(deps): bump taiki-e/install-action from 2.52.8 to 2.54.0 (#3681)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.52.8 to 2.54.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.52.8...v2.54.0)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.54.0
  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-06-23 10:39:16 +00:00
dependabot[bot] 5e0f56f572
build(deps): bump memchr from 2.7.4 to 2.7.5 (#3675)
Bumps [memchr](https://github.com/BurntSushi/memchr) from 2.7.4 to 2.7.5.
- [Commits](https://github.com/BurntSushi/memchr/compare/2.7.4...2.7.5)

---
updated-dependencies:
- dependency-name: memchr
  dependency-version: 2.7.5
  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-06-16 01:00:14 +00:00
dependabot[bot] 10266204ee
build(deps): bump cfg-if from 1.0.0 to 1.0.1 (#3674)
Bumps [cfg-if](https://github.com/rust-lang/cfg-if) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/rust-lang/cfg-if/releases)
- [Changelog](https://github.com/rust-lang/cfg-if/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cfg-if/compare/1.0.0...v1.0.1)

---
updated-dependencies:
- dependency-name: cfg-if
  dependency-version: 1.0.1
  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-06-16 01:00:13 +00:00
dependabot[bot] ebc8fdb29a
build(deps): bump slab from 0.4.9 to 0.4.10 (#3673)
Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.9 to 0.4.10.
- [Release notes](https://github.com/tokio-rs/slab/releases)
- [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/slab/compare/v0.4.9...v0.4.10)

---
updated-dependencies:
- dependency-name: slab
  dependency-version: 0.4.10
  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-06-16 01:00:12 +00:00
dependabot[bot] b61346e2ef
build(deps): bump taiki-e/install-action from 2.52.1 to 2.52.8 (#3672)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.52.1 to 2.52.8.
- [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.52.1...v2.52.8)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.52.8
  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-06-16 00:59:16 +00:00
dependabot[bot] db0a0512d9
build(deps): bump smallvec from 1.15.0 to 1.15.1 (#3667)
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.15.0 to 1.15.1.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.15.0...v1.15.1)

---
updated-dependencies:
- dependency-name: smallvec
  dependency-version: 1.15.1
  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-06-15 20:09:58 +00:00
dependabot[bot] eb1429d6dd
build(deps): bump flate2 from 1.1.1 to 1.1.2 (#3668)
Bumps [flate2](https://github.com/rust-lang/flate2-rs) from 1.1.1 to 1.1.2.
- [Release notes](https://github.com/rust-lang/flate2-rs/releases)
- [Commits](https://github.com/rust-lang/flate2-rs/compare/1.1.1...1.1.2)

---
updated-dependencies:
- dependency-name: flate2
  dependency-version: 1.1.2
  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-06-15 20:09:28 +00:00
dependabot[bot] 10f56f6412
build(deps): bump socket2 from 0.5.9 to 0.5.10 (#3664)
Bumps [socket2](https://github.com/rust-lang/socket2) from 0.5.9 to 0.5.10.
- [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-version: 0.5.10
  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-06-02 09:38:07 +00:00
dependabot[bot] 8562b7d7bb
build(deps): bump tokio from 1.45.0 to 1.45.1 (#3661)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.45.0 to 1.45.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.45.0...tokio-1.45.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.45.1
  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-05-26 12:50:31 +00:00
dependabot[bot] 442bb4cecf
build(deps): bump rustversion from 1.0.20 to 1.0.21 (#3660)
Bumps [rustversion](https://github.com/dtolnay/rustversion) from 1.0.20 to 1.0.21.
- [Release notes](https://github.com/dtolnay/rustversion/releases)
- [Commits](https://github.com/dtolnay/rustversion/compare/1.0.20...1.0.21)

---
updated-dependencies:
- dependency-name: rustversion
  dependency-version: 1.0.21
  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-05-26 10:37:38 +00:00
dependabot[bot] 9d98753ffa
build(deps): bump taiki-e/install-action from 2.51.2 to 2.52.1 (#3659)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.51.2 to 2.52.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.51.2...v2.52.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.52.1
  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-05-26 10:37:24 +00:00
Luca Cappelletti fd81e6ee0d
Resolved collisions between `missing_docs` clippy lints (#3653)
* Trying to resolve the clippy forbid(missing_doc) in web codegen

* Specifying to use workspace version

* Restored cargo toml as they were

* Restored toml and lock to original

* Replaced `#[doc(hidden)]` with documentation comments

* Reformatted code

* Removed auto-generated doc comment as existing doc comment should be fine
2025-05-24 15:50:14 +00:00
dependabot[bot] 1f9bc2de7d
build(deps): bump taiki-e/install-action from 2.50.10 to 2.51.2 (#3651)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.50.10 to 2.51.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.50.10...v2.51.2)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.51.2
  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-05-19 14:37:15 +00:00
dependabot[bot] 79f2486dc1
build(deps): bump codecov/codecov-action from 5.4.2 to 5.4.3 (#3652)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.2 to 5.4.3.
- [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.4.2...v5.4.3)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.4.3
  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-05-19 14:36:42 +00:00
dependabot[bot] 9a12bbd118
build(deps): bump bitflags from 2.9.0 to 2.9.1 (#3650)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.9.0 to 2.9.1.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.9.0...2.9.1)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-version: 2.9.1
  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-05-19 14:36:27 +00:00
dependabot[bot] 072fdc182d
build(deps): bump tempfile from 3.19.1 to 3.20.0 (#3646)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.19.1 to 3.20.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.19.1...v3.20.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.20.0
  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-05-12 01:12:31 +00:00
dependabot[bot] 347c7204a6
build(deps): bump trybuild from 1.0.104 to 1.0.105 (#3647)
Bumps [trybuild](https://github.com/dtolnay/trybuild) from 1.0.104 to 1.0.105.
- [Release notes](https://github.com/dtolnay/trybuild/releases)
- [Commits](https://github.com/dtolnay/trybuild/compare/1.0.104...1.0.105)

---
updated-dependencies:
- dependency-name: trybuild
  dependency-version: 1.0.105
  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-05-12 01:11:19 +00:00
Rob Ede 573090e01f
chore: update lockfile 2025-05-10 06:48:31 +01:00
Rob Ede 69dda5c943
ci: fix msrv job 2025-05-10 06:23:33 +01:00
Rob Ede 1b4b61d839
chore(awc): prepare release 3.7.0 2025-05-10 06:19:29 +01:00
Rob Ede 2c55d659dd
chore(actix-web): prepare release 4.11.0 2025-05-10 06:19:10 +01:00
Rob Ede 276f5d5bd4
chore(actix-http): prepare release 3.11.0 2025-05-10 06:18:25 +01:00
Rob Ede 5f3c02813a
chore: narrow tokio dep to account for RUSTSEC-2025-0023
closes #3640
2025-05-10 06:09:51 +01:00
Degubi 3d3b31e16a
fix: svg files should be compressed (#3486)
* Fix svg files not being compressed

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-10 03:21:36 +00:00
Joel Wurtz 3147aaccc7
feat: do not use host header on http2 for guard (#3525)
* feat(guard): do not use host header on http2 for guard

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-10 02:42:00 +00:00
Rob Ede 079400a72b
build: add clippy-msrv recipe 2025-05-10 03:21:59 +01:00
Björn Wärmedal a49f055561
build(deps): update url requirement from 2.1 to 2.5.4 (#3527)
Co-authored-by: Björn Wärmedal <bjorn.warmedal@lumera.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-10 02:00:20 +00:00
Rob Ede 55268b6898
fix: improve logger header values printing 2025-05-10 02:56:41 +01:00
Rob Ede 89b5b04653
docs: update docs about peer_addr when bound to a UDS socket 2025-05-10 02:23:22 +01:00
silverpill e42cffc28d
Fix HttpRequest::peer_addr documentation 2025-05-10 02:20:13 +01:00
Rob Ede 8765b04476
refactor: simplify on_connect 2025-05-10 02:19:56 +01:00
Rob Ede 40179e2aa6
docs: fix ConnectionInfo::realip_remote_addr documentation 2025-05-10 01:21:34 +01:00
JackSpagnoli 9bbb5414d1
Implements log_level for Logger middleware (#3605)
* implements log level for Logger

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-09 23:51:47 +00:00
Alexander 65f254d1b2
Re-export mime types for easier access #3603 (#3624)
* Re-export mime types for easier access #3603

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-09 23:40:21 +00:00
Sebastian Ziebell 25511dfb38
Fix order of template arguments in Handler documentation example (#3566)
Fix order of template arguments in doc
2025-05-09 23:31:59 +00:00
Rob Ede 6e902d1d5c
feat: add HttpServer::shutdown_signal (#3644) 2025-05-10 00:16:21 +01:00
dependabot[bot] f1b7cfb253
build(deps): bump tokio from 1.44.2 to 1.45.0 (#3643)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.2 to 1.45.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.2...tokio-1.45.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.45.0
  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-05-09 20:43:47 +00:00
dependabot[bot] e983276a78
build(deps): bump taiki-e/install-action from 2.50.7 to 2.50.10 (#3641)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.50.7 to 2.50.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.50.7...v2.50.10)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.50.10
  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-05-09 20:40:46 +00:00
dependabot[bot] bbe0134523
build(deps): bump brotli from 7.0.0 to 8.0.0 (#3627)
* build(deps): bump brotli from 7.0.0 to 8.0.0

Bumps [brotli](https://github.com/dropbox/rust-brotli) from 7.0.0 to 8.0.0.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits)

---
updated-dependencies:
- dependency-name: brotli
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* docs: update changelog

---------

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-05-09 20:05:56 +00:00
dependabot[bot] 2dd165dc0b
build(deps): bump divan from 0.1.18 to 0.1.21 (#3621)
Bumps [divan](https://github.com/nvzqz/divan) from 0.1.18 to 0.1.21.
- [Changelog](https://github.com/nvzqz/divan/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nvzqz/divan/compare/v0.1.18...v0.1.21)

---
updated-dependencies:
- dependency-name: divan
  dependency-version: 0.1.21
  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-05-09 20:05:19 +00:00
dependabot[bot] 9829f87cd1
build(deps): bump taiki-e/install-action from 2.49.50 to 2.50.7 (#3636)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.50 to 2.50.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.49.50...v2.50.7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.50.7
  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-05-09 19:32:24 +00:00
dependabot[bot] 6892896a62
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.11.0 to 1.12.0 (#3632)
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.11.0 to 1.12.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.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.12.0
  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-05-09 19:32:17 +00:00
dependabot[bot] bdb7b4da2d
build(deps): bump tokio-util from 0.7.14 to 0.7.15 (#3631)
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.14 to 0.7.15.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.14...tokio-util-0.7.15)

---
updated-dependencies:
- dependency-name: tokio-util
  dependency-version: 0.7.15
  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-05-09 19:32:05 +00:00
Rob Ede 7eea3d3657
chore: address clippy lints 2025-05-09 20:21:02 +01:00
Rob Ede ad73cdc823
build: rework justfile toolchain 2025-05-09 20:15:43 +01:00
dependabot[bot] e27e1e3806
build(deps): bump taiki-e/install-action from 2.49.45 to 2.49.50 (#3629)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.45 to 2.49.50.
- [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.45...v2.49.50)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.49.50
  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-04-22 00:46:43 +00:00
dependabot[bot] 5939af88b3
build(deps): bump codecov/codecov-action from 5.4.0 to 5.4.2 (#3628)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.0 to 5.4.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.4.0...v5.4.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.4.2
  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-04-22 00:46:28 +00:00
dependabot[bot] 741ca85d4a
build(deps): bump proc-macro2 from 1.0.94 to 1.0.95 (#3626)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.94 to 1.0.95.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.94...1.0.95)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-version: 1.0.95
  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-04-22 00:46:11 +00:00
dependabot[bot] b8a612d21e
build(deps): bump rand from 0.9.0 to 0.9.1 (#3625)
Bumps [rand](https://github.com/rust-random/rand) from 0.9.0 to 0.9.1.
- [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.9.0...rand_core-0.9.1)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.9.1
  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-04-22 00:45:57 +00:00
dependabot[bot] 74c1c3de59
build(deps): bump taiki-e/install-action from 2.49.39 to 2.49.45 (#3618)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.39 to 2.49.45.
- [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.39...v2.49.45)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.49.45
  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-04-09 20:34:30 +00:00
dependabot[bot] 6b9c47b0ec
build(deps): bump env_logger from 0.11.7 to 0.11.8 (#3616)
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.11.7 to 0.11.8.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.11.7...v0.11.8)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-version: 0.11.8
  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-04-09 20:34:28 +00:00
Rob Ede 9915defca3
chore: update deps 2025-04-09 21:43:19 +01:00
dependabot[bot] 0ef246a846
build(deps): bump openssl from 0.10.71 to 0.10.72 (#3612)
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.71 to 0.10.72.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.71...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-05 21:32:30 +00:00
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
196 changed files with 9139 additions and 2785 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"

8
.clippy.toml Normal file
View File

@ -0,0 +1,8 @@
disallowed-names = [
"..",
"e", # no single letter error bindings
]
disallowed-methods = [
{ path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
{ path = "std::rc::Rc::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
]

15
.cspell.yml Normal file
View File

@ -0,0 +1,15 @@
version: "0.2"
words:
- actix
- addrs
- ALPN
- bytestring
- httparse
- MSRV
- realip
- rustls
- rustup
- serde
- uring
- webpki
- zstd

2
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [robjtede] github: [robjtede, JohnTitor]

View File

@ -1,10 +1,11 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
versioning-strategy: lockfile-only

View File

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

View File

@ -2,7 +2,7 @@ name: CI (post-merge)
on: on:
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -28,11 +28,11 @@ jobs:
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- 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@72793074d3c8cdda771dba85f6deafe00623038b # 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.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.38.0 uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
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
@ -71,21 +71,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- 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@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
- name: Install cargo-hack - name: Install just, cargo-hack
uses: taiki-e/install-action@v2.38.0 uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
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

@ -6,7 +6,7 @@ on:
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -18,7 +18,7 @@ concurrency:
jobs: jobs:
read_msrv: read_msrv:
name: Read MSRV name: Read MSRV
uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0 uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@8b553824444060021f2843d7b4d803f3624d15e5 # v0.1.0
build_and_test: build_and_test:
needs: read_msrv needs: read_msrv
@ -39,11 +39,11 @@ jobs:
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- 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@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
@ -56,15 +56,15 @@ jobs:
- name: Setup mold linker - name: Setup mold linker
if: matrix.target.os == 'ubuntu-latest' if: matrix.target.os == 'ubuntu-latest'
uses: rui314/setup-mold@v1 uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.38.0 uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
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
@ -85,14 +85,18 @@ jobs:
- name: CI cache clean - name: CI cache clean
run: cargo-ci-cache-clean run: cargo-ci-cache-clean
- name: deny check
if: matrix.version.name == 'stable' && matrix.target.os == 'ubuntu-latest'
uses: EmbarkStudios/cargo-deny-action@76cd80eb775d7bbbd2d80292136d74d39e1b4918 # v2.0.14
io-uring: io-uring:
name: io-uring tests name: io-uring tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
@ -105,15 +109,15 @@ jobs:
name: doc tests name: doc tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
- name: Install just - name: Install just
uses: taiki-e/install-action@v2.38.0 uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with: with:
tool: just tool: just

View File

@ -2,7 +2,7 @@ name: Coverage
on: on:
push: push:
branches: [master] branches: [main]
permissions: permissions:
contents: read contents: read
@ -15,16 +15,16 @@ jobs:
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
components: llvm-tools components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest - name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@v2.38.0 uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with: with:
tool: just,cargo-llvm-cov,cargo-nextest tool: just,cargo-llvm-cov,cargo-nextest
@ -32,7 +32,7 @@ jobs:
run: just test-coverage-codecov run: just test-coverage-codecov
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4.4.1 uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with: with:
files: codecov.json files: codecov.json
fail_ci_if_error: true fail_ci_if_error: true

View File

@ -15,10 +15,10 @@ jobs:
fmt: fmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
components: rustfmt components: rustfmt
@ -33,15 +33,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
components: clippy components: clippy
- name: Check with Clippy - name: Check with Clippy
uses: giraffate/clippy-action@v1.0.1 uses: giraffate/clippy-action@13b9d32482f25d29ead141b79e7e04e7900281e0 # v1.0.1
with: with:
reporter: github-pr-check reporter: github-pr-check
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
@ -52,10 +52,10 @@ jobs:
lint-docs: lint-docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly toolchain: nightly
components: rust-docs components: rust-docs
@ -66,51 +66,25 @@ jobs:
run: cargo +nightly doc --no-deps --workspace --all-features run: cargo +nightly doc --no-deps --workspace --all-features
check-external-types: check-external-types:
if: false # rustdoc mismatch currently
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Rust (nightly-2024-05-01) - name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
with: with:
toolchain: nightly-2024-05-01 toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just - name: Install just
uses: taiki-e/install-action@v2.38.0 uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
with: with:
tool: just tool: just
- name: Install cargo-check-external-types - name: Install cargo-check-external-types
uses: taiki-e/cache-cargo-install-action@v1.2.2 uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # v2.3.1
with: with:
tool: cargo-check-external-types tool: cargo-check-external-types
- name: check external types - name: check external types
run: just check-external-types-all +nightly-2024-05-01 run: just check-external-types-all +${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
public-api-diff:
runs-on: ubuntu-latest
steps:
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Checkout PR branch
uses: actions/checkout@v4
- name: Install Rust (nightly-2024-06-07)
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
with:
toolchain: nightly-2024-06-07
- name: Install cargo-public-api
uses: taiki-e/install-action@v2.38.0
with:
tool: cargo-public-api
- name: Generate API diff
run: |
for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do
cargo public-api --manifest-path "$f" --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

38
.taplo.toml Normal file
View File

@ -0,0 +1,38 @@
exclude = ["target/*"]
include = ["**/*.toml"]
[formatting]
column_width = 100
align_comments = false
[[rule]]
include = ["**/Cargo.toml"]
keys = ["features"]
formatting.column_width = 105
formatting.reorder_keys = false
[[rule]]
include = ["**/Cargo.toml"]
keys = [
"dependencies",
"*-dependencies",
"workspace.dependencies",
"workspace.*-dependencies",
"target.*.dependencies",
"target.*.*-dependencies",
]
formatting.column_width = 120
formatting.reorder_keys = true
[[rule]]
include = ["**/Cargo.toml"]
keys = [
"dependencies.*",
"*-dependencies.*",
"workspace.dependencies.*",
"workspace.*-dependencies.*",
"target.*.dependencies",
"target.*.*-dependencies",
]
formatting.column_width = 120
formatting.reorder_keys = false

3893
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.82"
[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,22 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.82.
## 0.6.9
- Correct `derive_more` dependency feature requirements.
## 0.6.8
- Add `Files::with_permanent_redirect()` method.
- Change default redirect status code to 307 Temporary Redirect.
## 0.6.7
- Add `{Files, NamedFile}::read_mode_threshold()` methods to allow faster synchronous reads of small files.
- 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

@ -1,10 +1,7 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.6" version = "0.6.9"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Static file serving for Actix Web" description = "Static file serving for Actix Web"
keywords = ["actix", "http", "async", "futures"] keywords = ["actix", "http", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -14,13 +11,7 @@ license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = ["actix_http::*", "actix_service::*", "actix_web::*", "http::*", "mime::*"]
"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"]
@ -33,7 +24,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 = ["deref", "deref_mut", "display", "error", "from"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http-range = "0.1.4" http-range = "0.1.4"
log = "0.4" log = "0.4"
@ -54,3 +45,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

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

View File

@ -14,6 +14,12 @@ use pin_project_lite::pin_project;
use super::named::File; use super::named::File;
#[derive(Debug, Clone, Copy)]
pub(crate) enum ReadMode {
Sync,
Async,
}
pin_project! { pin_project! {
/// Adapter to read a `std::file::File` in chunks. /// Adapter to read a `std::file::File` in chunks.
#[doc(hidden)] #[doc(hidden)]
@ -24,6 +30,7 @@ pin_project! {
state: ChunkedReadFileState<Fut>, state: ChunkedReadFileState<Fut>,
counter: u64, counter: u64,
callback: F, callback: F,
read_mode: ReadMode,
} }
} }
@ -57,6 +64,7 @@ pub(crate) fn new_chunked_read(
size: u64, size: u64,
offset: u64, offset: u64,
file: File, file: File,
read_mode_threshold: u64,
) -> impl Stream<Item = Result<Bytes, Error>> { ) -> impl Stream<Item = Result<Bytes, Error>> {
ChunkedReadFile { ChunkedReadFile {
size, size,
@ -69,18 +77,22 @@ pub(crate) fn new_chunked_read(
}, },
counter: 0, counter: 0,
callback: chunked_read_file_callback, callback: chunked_read_file_callback,
read_mode: if size < read_mode_threshold {
ReadMode::Sync
} else {
ReadMode::Async
},
} }
} }
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
async fn chunked_read_file_callback( fn chunked_read_file_callback_sync(
mut file: File, mut file: File,
offset: u64, offset: u64,
max_bytes: usize, max_bytes: usize,
) -> Result<(File, Bytes), Error> { ) -> Result<(File, Bytes), io::Error> {
use io::{Read as _, Seek as _}; use io::{Read as _, Seek as _};
let res = actix_web::web::block(move || {
let mut buf = Vec::with_capacity(max_bytes); let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?; file.seek(io::SeekFrom::Start(offset))?;
@ -92,8 +104,23 @@ async fn chunked_read_file_callback(
} else { } else {
Ok((file, Bytes::from(buf))) Ok((file, Bytes::from(buf)))
} }
}) }
.await??;
#[cfg(not(feature = "experimental-io-uring"))]
#[inline]
async fn chunked_read_file_callback(
file: File,
offset: u64,
max_bytes: usize,
read_mode: ReadMode,
) -> Result<(File, Bytes), Error> {
let res = match read_mode {
ReadMode::Sync => chunked_read_file_callback_sync(file, offset, max_bytes)?,
ReadMode::Async => {
actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes))
.await??
}
};
Ok(res) Ok(res)
} }
@ -171,7 +198,7 @@ where
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
impl<F, Fut> Stream for ChunkedReadFile<F, Fut> impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
where where
F: Fn(File, u64, usize) -> Fut, F: Fn(File, u64, usize, ReadMode) -> Fut,
Fut: Future<Output = Result<(File, Bytes), Error>>, Fut: Future<Output = Result<(File, Bytes), Error>>,
{ {
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
@ -193,7 +220,7 @@ where
.take() .take()
.expect("ChunkedReadFile polled after completion"); .expect("ChunkedReadFile polled after completion");
let fut = (this.callback)(file, offset, max_bytes); let fut = (this.callback)(file, offset, max_bytes, *this.read_mode);
this.state this.state
.project_replace(ChunkedReadFileState::Future { fut }); .project_replace(ChunkedReadFileState::Future { fut });

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

@ -41,6 +41,7 @@ pub struct Files {
index: Option<String>, index: Option<String>,
show_index: bool, show_index: bool,
redirect_to_slash: bool, redirect_to_slash: bool,
with_permanent_redirect: bool,
default: Rc<RefCell<Option<Rc<HttpNewService>>>>, default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
renderer: Rc<DirectoryRenderer>, renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>, mime_override: Option<Rc<MimeOverride>>,
@ -49,6 +50,7 @@ pub struct Files {
use_guards: Option<Rc<dyn Guard>>, use_guards: Option<Rc<dyn Guard>>,
guards: Vec<Rc<dyn Guard>>, guards: Vec<Rc<dyn Guard>>,
hidden_files: bool, hidden_files: bool,
read_mode_threshold: u64,
} }
impl fmt::Debug for Files { impl fmt::Debug for Files {
@ -64,6 +66,7 @@ impl Clone for Files {
index: self.index.clone(), index: self.index.clone(),
show_index: self.show_index, show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash, redirect_to_slash: self.redirect_to_slash,
with_permanent_redirect: self.with_permanent_redirect,
default: self.default.clone(), default: self.default.clone(),
renderer: self.renderer.clone(), renderer: self.renderer.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
@ -73,6 +76,7 @@ impl Clone for Files {
use_guards: self.use_guards.clone(), use_guards: self.use_guards.clone(),
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
read_mode_threshold: self.read_mode_threshold,
} }
} }
} }
@ -111,6 +115,7 @@ impl Files {
index: None, index: None,
show_index: false, show_index: false,
redirect_to_slash: false, redirect_to_slash: false,
with_permanent_redirect: false,
default: Rc::new(RefCell::new(None)), default: Rc::new(RefCell::new(None)),
renderer: Rc::new(directory_listing), renderer: Rc::new(directory_listing),
mime_override: None, mime_override: None,
@ -119,6 +124,7 @@ impl Files {
use_guards: None, use_guards: None,
guards: Vec::new(), guards: Vec::new(),
hidden_files: false, hidden_files: false,
read_mode_threshold: 0,
} }
} }
@ -141,6 +147,14 @@ impl Files {
self self
} }
/// Redirect with permanent redirect status code (308).
///
/// By default redirect with temporary redirect status code (307).
pub fn with_permanent_redirect(mut self) -> Self {
self.with_permanent_redirect = true;
self
}
/// Set custom directory renderer. /// Set custom directory renderer.
pub fn files_listing_renderer<F>(mut self, f: F) -> Self pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where where
@ -204,6 +218,23 @@ impl Files {
self self
} }
/// Sets the size threshold that determines file read mode (sync/async).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
///
/// Tweaking this value according to your expected usage may lead to signifiant performance
/// gains (or losses in other handlers, if `size` is too high).
///
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
///
/// Default is 0, meaning all files are read asynchronously.
pub fn read_mode_threshold(mut self, size: u64) -> Self {
self.read_mode_threshold = size;
self
}
/// Specifies whether to use ETag or not. /// Specifies whether to use ETag or not.
/// ///
/// Default is true. /// Default is true.
@ -367,6 +398,8 @@ impl ServiceFactory<ServiceRequest> for Files {
file_flags: self.file_flags, file_flags: self.file_flags,
guards: self.use_guards.clone(), guards: self.use_guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
size_threshold: self.read_mode_threshold,
with_permanent_redirect: self.with_permanent_redirect,
}; };
if let Some(ref default) = *self.default.borrow() { if let Some(ref default) = *self.default.borrow() {

View File

@ -11,11 +11,10 @@
//! .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_cfg))]
use std::path::Path; use std::path::Path;
@ -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\"",
); );
} }
@ -737,7 +736,21 @@ mod tests {
.await; .await;
let req = TestRequest::with_uri("/tests").to_request(); let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
// should redirect if index present with permanent redirect
let srv = test::init_service(
App::new().service(
Files::new("/", ".")
.index_file("test.png")
.redirect_to_slash_directory()
.with_permanent_redirect(),
),
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::PERMANENT_REDIRECT);
// should redirect if files listing is enabled // should redirect if files listing is enabled
let srv = test::init_service( let srv = test::init_service(
@ -750,7 +763,7 @@ mod tests {
.await; .await;
let req = TestRequest::with_uri("/tests").to_request(); let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
// should not redirect if the path is wrong // should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request(); let req = TestRequest::with_uri("/not_existing").to_request();

View File

@ -80,6 +80,7 @@ pub struct NamedFile {
pub(crate) content_type: Mime, pub(crate) content_type: Mime,
pub(crate) content_disposition: ContentDisposition, pub(crate) content_disposition: ContentDisposition,
pub(crate) encoding: Option<ContentEncoding>, pub(crate) encoding: Option<ContentEncoding>,
pub(crate) read_mode_threshold: u64,
} }
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
@ -200,6 +201,7 @@ impl NamedFile {
encoding, encoding,
status_code: StatusCode::OK, status_code: StatusCode::OK,
flags: Flags::default(), flags: Flags::default(),
read_mode_threshold: 0,
}) })
} }
@ -353,6 +355,23 @@ impl NamedFile {
self self
} }
/// Sets the size threshold that determines file read mode (sync/async).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
///
/// Tweaking this value according to your expected usage may lead to signifiant performance
/// gains (or losses in other handlers, if `size` is too high).
///
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
///
/// Default is 0, meaning all files are read asynchronously.
pub fn read_mode_threshold(mut self, size: u64) -> Self {
self.read_mode_threshold = size;
self
}
/// Specifies whether to return `ETag` header in response. /// Specifies whether to return `ETag` header in response.
/// ///
/// Default is true. /// Default is true.
@ -440,7 +459,8 @@ impl NamedFile {
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str())); res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
} }
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); let reader =
chunked::new_chunked_read(self.md.len(), 0, self.file, self.read_mode_threshold);
return res.streaming(reader); return res.streaming(reader);
} }
@ -577,7 +597,7 @@ impl NamedFile {
.map_into_boxed_body(); .map_into_boxed_body();
} }
let reader = chunked::new_chunked_read(length, offset, self.file); let reader = chunked::new_chunked_read(length, offset, self.file, self.read_mode_threshold);
if offset != 0 || length != self.md.len() { if offset != 0 || length != self.md.len() {
res.status(StatusCode::PARTIAL_CONTENT); res.status(StatusCode::PARTIAL_CONTENT);

View File

@ -39,6 +39,8 @@ pub struct FilesServiceInner {
pub(crate) file_flags: named::Flags, pub(crate) file_flags: named::Flags,
pub(crate) guards: Option<Rc<dyn Guard>>, pub(crate) guards: Option<Rc<dyn Guard>>,
pub(crate) hidden_files: bool, pub(crate) hidden_files: bool,
pub(crate) size_threshold: u64,
pub(crate) with_permanent_redirect: bool,
} }
impl fmt::Debug for FilesServiceInner { impl fmt::Debug for FilesServiceInner {
@ -70,7 +72,9 @@ impl FilesService {
named_file.flags = self.file_flags; named_file.flags = self.file_flags;
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); let res = named_file
.read_mode_threshold(self.size_threshold)
.into_response(&req);
ServiceResponse::new(req, res) ServiceResponse::new(req, res)
} }
@ -79,7 +83,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))
} }
} }
@ -145,11 +149,15 @@ impl Service<ServiceRequest> for FilesService {
{ {
let redirect_to = format!("{}/", req.path()); let redirect_to = format!("{}/", req.path());
return Ok(req.into_response( let response = if this.with_permanent_redirect {
HttpResponse::Found() HttpResponse::PermanentRedirect()
} else {
HttpResponse::TemporaryRedirect()
}
.insert_header((header::LOCATION, redirect_to)) .insert_header((header::LOCATION, redirect_to))
.finish(), .finish();
));
return Ok(req.into_response(response));
} }
match this.index { match this.index {
@ -169,17 +177,7 @@ impl Service<ServiceRequest> for FilesService {
} }
} else { } else {
match NamedFile::open_async(&path).await { match NamedFile::open_async(&path).await {
Ok(mut named_file) => { Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
if let Some(ref mime_override) = this.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = this.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
Ok(ServiceResponse::new(req, res))
}
Err(err) => this.handle_err(err, req).await, Err(err) => this.handle_err(err, req).await,
} }
} }

View File

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

View File

@ -37,25 +37,28 @@ default = []
openssl = ["tls-openssl", "awc/openssl"] openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2"
actix-codec = "0.5" actix-codec = "0.5"
actix-tls = "3"
actix-utils = "3"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2" actix-server = "2"
actix-service = "2"
actix-tls = "3"
actix-utils = "3"
awc = { version = "3", default-features = false } awc = { version = "3", default-features = false }
bytes = "1" bytes = "1"
futures-core = { version = "0.3.17", default-features = false } futures-core = { version = "0.3.17", default-features = false }
http = "0.2.7" http = "0.2.7"
log = "0.4" log = "0.4"
socket2 = "0.5"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
slab = "0.4"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
slab = "0.4"
socket2 = "0.6"
tls-openssl = { version = "0.10.55", package = "openssl", optional = true } tls-openssl = { version = "0.10.55", package = "openssl", optional = true }
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-http = "3" actix-http = "3"
[lints]
workspace = true

View File

@ -1,10 +1,8 @@
//! 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_cfg))]
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
@ -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,47 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.82.
## 3.11.2
- Properly wake Payload receivers when feeding errors or EOF.
- Add `ServiceConfigBuilder` type to facilitate future configuration extensions.
- Add a configuration option to allow/disallow half closed connections in HTTP/1. This defaults to allow, reverting the change made in 3.11.1.
- Shutdown connections when HTTP Responses are written without reading full Requests.
## 3.11.1
- Prevent more hangs after client disconnects.
- More malformed WebSocket frames are now gracefully rejected.
- Using `TestRequest::set_payload()` now sets a Content-Length header.
## 3.11.0
- Update `brotli` dependency to `8`.
## 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,10 +1,7 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.7.0" version = "3.11.2"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "HTTP types and services for the Actix ecosystem" description = "HTTP types and services for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -20,7 +17,6 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [ features = [
"http2", "http2",
"ws", "ws",
@ -59,55 +55,54 @@ allowed_external_types = [
default = [] default = []
# HTTP/2 protocol support # HTTP/2 protocol support
http2 = ["h2"] http2 = ["dep:h2"]
# WebSocket protocol implementation # WebSocket protocol implementation
ws = [ ws = ["dep:local-channel", "dep:base64", "dep:rand", "dep:sha1"]
"local-channel",
"base64",
"rand",
"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 = []
[dependencies] # Internal (PRIVATE!) features used to aid checking feature status.
actix-service = "2" # Don't rely on these whatsoever. They may disappear at anytime.
actix-codec = "0.5" __tls = []
actix-utils = "3"
actix-rt = { version = "2.2", default-features = false } [dependencies]
actix-codec = "0.5"
actix-rt = { version = "2.2", default-features = false }
actix-service = "2"
actix-utils = "3"
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"
@ -118,24 +113,24 @@ mime = "0.3.4"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2" pin-project-lite = "0.2"
smallvec = "1.6.1" smallvec = "1.6.1"
tokio = { version = "1.24.2", features = [] } tokio = { version = "1.38.2", features = [] }
tokio-util = { version = "0.7", features = ["io", "codec"] } tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# http2 # http2
h2 = { version = "0.3.26", optional = true } h2 = { version = "0.3.27", optional = true }
# websockets # websockets
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 } local-channel = { version = "0.1", 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 = "8", 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 }
@ -151,17 +146,20 @@ 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"
rustls-pemfile = "2" rustls-pemfile = "2"
serde = { version = "1.0", features = ["derive"] } rustversion = "1"
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.38.2", features = ["net", "rt", "macros", "sync"] }
[lints]
workspace = true
[[example]] [[example]]
name = "ws" name = "ws"

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

View File

@ -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::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;
@ -190,7 +190,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn to_body_limit_error() { async fn to_body_limit_error() {
let err_stream = stream::once(async { Err(io::Error::new(io::ErrorKind::Other, "")) }); let err_stream = stream::once(async { Err(io::Error::other("")) });
let body = SizedStream::new(8, err_stream); let body = SizedStream::new(8, err_stream);
// not too big, but propagates error from body stream // not too big, but propagates error from body stream
assert!(to_bytes_limited(body, 10).await.unwrap().is_err()); assert!(to_bytes_limited(body, 10).await.unwrap().is_err());

View File

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

View File

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

View File

@ -100,10 +100,7 @@ where
loop { loop {
if let Some(ref mut fut) = this.fut { if let Some(ref mut fut) = this.fut {
let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| { let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| {
PayloadError::Io(io::Error::new( PayloadError::Io(io::Error::other("Blocking task was cancelled unexpectedly"))
io::ErrorKind::Other,
"Blocking task was cancelled unexpectedly",
))
})??; })??;
*this.decoder = Some(decoder); *this.decoder = Some(decoder);

View File

@ -183,8 +183,7 @@ where
if let Some(ref mut fut) = this.fut { if let Some(ref mut fut) = this.fut {
let mut encoder = ready!(Pin::new(fut).poll(cx)) let mut encoder = ready!(Pin::new(fut).poll(cx))
.map_err(|_| { .map_err(|_| {
EncoderError::Io(io::Error::new( EncoderError::Io(io::Error::other(
io::ErrorKind::Other,
"Blocking task was cancelled unexpectedly", "Blocking task was cancelled unexpectedly",
)) ))
})? })?
@ -415,11 +414,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,
} }
@ -415,7 +415,7 @@ mod tests {
#[test] #[test]
fn test_as_response() { fn test_as_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::other("other");
let err: Error = ParseError::Io(orig).into(); let err: Error = ParseError::Io(orig).into();
assert_eq!( assert_eq!(
format!("{}", err), format!("{}", err),
@ -425,14 +425,14 @@ mod tests {
#[test] #[test]
fn test_error_display() { fn test_error_display() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::other("other");
let err = Error::new_io().with_cause(orig); let err = Error::new_io().with_cause(orig);
assert_eq!("connection error: other", err.to_string()); assert_eq!("connection error: other", err.to_string());
} }
#[test] #[test]
fn test_error_http_response() { fn test_error_http_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::other("other");
let err = Error::new_io().with_cause(orig); let err = Error::new_io().with_cause(orig);
let resp: Response<BoxBody> = err.into(); let resp: Response<BoxBody> = err.into();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
@ -440,7 +440,7 @@ mod tests {
#[test] #[test]
fn test_payload_error() { fn test_payload_error() {
let err: PayloadError = io::Error::new(io::ErrorKind::Other, "ParseError").into(); let err: PayloadError = io::Error::other("ParseError").into();
assert!(err.to_string().contains("ParseError")); assert!(err.to_string().contains("ParseError"));
let err = PayloadError::Incomplete(None); let err = PayloadError::Incomplete(None);
@ -475,7 +475,7 @@ mod tests {
#[test] #[test]
fn test_from() { fn test_from() {
from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => ParseError::Io(..)); from_and_cause!(io::Error::other("other") => ParseError::Io(..));
from!(httparse::Error::HeaderName => ParseError::Header); from!(httparse::Error::HeaderName => ParseError::Header);
from!(httparse::Error::HeaderName => ParseError::Header); from!(httparse::Error::HeaderName => ParseError::Header);
from!(httparse::Error::HeaderValue => ParseError::Header); from!(httparse::Error::HeaderValue => ParseError::Header);

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

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

View File

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

View File

@ -310,10 +310,10 @@ impl MessageType for RequestHeadType {
Version::HTTP_11 => "HTTP/1.1", Version::HTTP_11 => "HTTP/1.1",
Version::HTTP_2 => "HTTP/2.0", Version::HTTP_2 => "HTTP/2.0",
Version::HTTP_3 => "HTTP/3.0", Version::HTTP_3 => "HTTP/3.0",
_ => return Err(io::Error::new(io::ErrorKind::Other, "unsupported version")), _ => return Err(io::Error::other("Unsupported version")),
} }
) )
.map_err(|e| io::Error::new(io::ErrorKind::Other, e)) .map_err(io::Error::other)
} }
} }
@ -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(io::Error::other)?;
buf.reserve(msg.len() + 2); buf.reserve(msg.len() + 2);
buf.extend_from_slice(msg); buf.extend_from_slice(msg);

View File

@ -200,11 +200,13 @@ impl Inner {
#[inline] #[inline]
fn set_error(&mut self, err: PayloadError) { fn set_error(&mut self, err: PayloadError) {
self.err = Some(err); self.err = Some(err);
self.wake();
} }
#[inline] #[inline]
fn feed_eof(&mut self) { fn feed_eof(&mut self) {
self.eof = true; self.eof = true;
self.wake();
} }
#[inline] #[inline]
@ -253,8 +255,13 @@ impl Inner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{task::Poll, time::Duration};
use actix_rt::time::timeout;
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use futures_util::{FutureExt, StreamExt};
use static_assertions::{assert_impl_all, assert_not_impl_any}; use static_assertions::{assert_impl_all, assert_not_impl_any};
use tokio::sync::oneshot;
use super::*; use super::*;
@ -263,6 +270,67 @@ mod tests {
assert_impl_all!(Inner: Unpin, Send, Sync); assert_impl_all!(Inner: Unpin, Send, Sync);
const WAKE_TIMEOUT: Duration = Duration::from_secs(2);
fn prepare_waking_test(
mut payload: Payload,
expected: Option<Result<(), ()>>,
) -> (oneshot::Receiver<()>, actix_rt::task::JoinHandle<()>) {
let (tx, rx) = oneshot::channel();
let handle = actix_rt::spawn(async move {
// Make sure to poll once to set the waker
poll_fn(|cx| {
assert!(payload.poll_next_unpin(cx).is_pending());
Poll::Ready(())
})
.await;
tx.send(()).unwrap();
// actix-rt is single-threaded, so this won't race with `rx.await`
let mut pend_once = false;
poll_fn(|_| {
if pend_once {
Poll::Ready(())
} else {
// Return pending without storing wakers, we already did on the previous
// `poll_fn`, now this task will only continue if the `sender` wakes us
pend_once = true;
Poll::Pending
}
})
.await;
let got = payload.next().now_or_never().unwrap();
match expected {
Some(Ok(_)) => assert!(got.unwrap().is_ok()),
Some(Err(_)) => assert!(got.unwrap().is_err()),
None => assert!(got.is_none()),
}
});
(rx, handle)
}
#[actix_rt::test]
async fn wake_on_error() {
let (mut sender, payload) = Payload::create(false);
let (rx, handle) = prepare_waking_test(payload, Some(Err(())));
rx.await.unwrap();
sender.set_error(PayloadError::Incomplete(None));
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test]
async fn wake_on_eof() {
let (mut sender, payload) = Payload::create(false);
let (rx, handle) = prepare_waking_test(payload, None);
rx.await.unwrap();
sender.feed_eof();
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {
let (_, mut payload) = Payload::create(false); let (_, mut payload) = Payload::create(false);

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,9 +6,9 @@
//! | ------------------- | ------------------------------------------- | //! | ------------------- | ------------------------------------------- |
//! | `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. |
@ -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,
@ -29,7 +27,7 @@
)] )]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
pub use http::{uri, uri::Uri, Method, StatusCode, Version}; pub use http::{uri, uri::Uri, Method, StatusCode, Version};
@ -61,17 +59,11 @@ 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,
config::ServiceConfig, config::{ServiceConfig, ServiceConfigBuilder},
error::Error, error::Error,
extensions::Extensions, extensions::Extensions,
header::ContentEncoding, header::ContentEncoding,

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

@ -11,7 +11,7 @@ use std::{
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::{Method, Uri, Version}; use http::{header, Method, Uri, Version};
use crate::{ use crate::{
header::{HeaderMap, TryIntoHeaderPair}, header::{HeaderMap, TryIntoHeaderPair},
@ -98,9 +98,13 @@ impl TestRequest {
} }
/// Set request payload. /// Set request payload.
///
/// This sets the `Content-Length` header with the size of `data`.
pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self { pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self {
let mut payload = crate::h1::Payload::empty(); let mut payload = crate::h1::Payload::empty();
payload.unread_data(data.into()); let bytes = data.into();
self.insert_header((header::CONTENT_LENGTH, bytes.len()));
payload.unread_data(bytes);
parts(&mut self.0).payload = Some(payload.into()); parts(&mut self.0).payload = Some(payload.into());
self self
} }
@ -159,8 +163,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(),
} }
} }
@ -271,6 +275,7 @@ impl TestSeqBuffer {
{ {
Self(Rc::new(RefCell::new(TestSeqInner { Self(Rc::new(RefCell::new(TestSeqInner {
read_buf: data.into(), read_buf: data.into(),
read_closed: false,
write_buf: BytesMut::new(), write_buf: BytesMut::new(),
err: None, err: None,
}))) })))
@ -289,36 +294,59 @@ impl TestSeqBuffer {
Ref::map(self.0.borrow(), |inner| &inner.write_buf) Ref::map(self.0.borrow(), |inner| &inner.write_buf)
} }
pub fn take_write_buf(&self) -> Bytes {
self.0.borrow_mut().write_buf.split().freeze()
}
pub fn err(&self) -> Ref<'_, Option<io::Error>> { pub fn err(&self) -> Ref<'_, Option<io::Error>> {
Ref::map(self.0.borrow(), |inner| &inner.err) Ref::map(self.0.borrow(), |inner| &inner.err)
} }
/// Add data to read buffer. /// Add data to read buffer.
///
/// # Panics
///
/// Panics if called after [`TestSeqBuffer::close_read`] has been called
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) { pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
self.0 let mut inner = self.0.borrow_mut();
.borrow_mut() if inner.read_closed {
.read_buf panic!("Tried to extend the read buffer after calling close_read");
.extend_from_slice(data.as_ref()) }
inner.read_buf.extend_from_slice(data.as_ref())
}
/// Closes the [`AsyncRead`]/[`Read`] part of this test buffer.
///
/// The current data in the buffer will still be returned by a call to read/poll_read, however,
/// after the buffer is empty, it will return `Ok(0)` to signify the EOF condition
pub fn close_read(&self) {
self.0.borrow_mut().read_closed = true;
} }
} }
pub struct TestSeqInner { pub struct TestSeqInner {
read_buf: BytesMut, read_buf: BytesMut,
read_closed: bool,
write_buf: BytesMut, write_buf: BytesMut,
err: Option<io::Error>, err: Option<io::Error>,
} }
impl io::Read for TestSeqBuffer { impl io::Read for TestSeqBuffer {
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> { fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
if self.0.borrow().read_buf.is_empty() { let mut inner = self.0.borrow_mut();
if self.0.borrow().err.is_some() {
Err(self.0.borrow_mut().err.take().unwrap()) if inner.read_buf.is_empty() {
if let Some(err) = inner.err.take() {
Err(err)
} else if inner.read_closed {
Ok(0)
} else { } else {
Err(io::Error::new(io::ErrorKind::WouldBlock, "")) Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
} }
} else { } else {
let size = std::cmp::min(self.0.borrow().read_buf.len(), dst.len()); let size = std::cmp::min(inner.read_buf.len(), dst.len());
let b = self.0.borrow_mut().read_buf.split_to(size); let b = inner.read_buf.split_to(size);
dst[..size].copy_from_slice(&b); dst[..size].copy_from_slice(&b);
Ok(size) Ok(size)
} }

View File

@ -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

@ -94,11 +94,21 @@ impl Parser {
Some(res) => res, Some(res) => res,
}; };
let frame_len = match idx.checked_add(length) {
Some(len) => len,
None => return Err(ProtocolError::Overflow),
};
// not enough data // not enough data
if src.len() < idx + length { if src.len() < frame_len {
let min_length = min(length, max_size); let min_length = min(length, max_size);
if src.capacity() < idx + min_length { let required_cap = match idx.checked_add(min_length) {
src.reserve(idx + min_length - src.capacity()); Some(cap) => cap,
None => return Err(ProtocolError::Overflow),
};
if src.capacity() < required_cap {
src.reserve(required_cap - src.capacity());
} }
return Ok(None); return Ok(None);
} }
@ -402,4 +412,14 @@ mod tests {
Parser::write_close(&mut buf, None, false); Parser::write_close(&mut buf, None, false);
assert_eq!(&buf[..], &vec![0x88, 0x00][..]); assert_eq!(&buf[..], &vec![0x88, 0x00][..]);
} }
#[test]
fn test_parse_length_overflow() {
let buf: [u8; 14] = [
0x0a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xeb, 0x0e, 0x8f,
];
let mut buf = BytesMut::from(&buf[..]);
let result = Parser::parse(&mut buf, true, 65536);
assert!(matches!(result, Err(ProtocolError::Overflow)));
}
} }

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,10 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.82.
## 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"]
@ -11,21 +11,23 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true all-features = true
[lib] [lib]
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_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, Failed to parse unit "byt..."
--> tests/trybuild/size-limit-parse-fail.rs:6:5 --> tests/trybuild/size-limit-parse-fail.rs:6:5
| |
6 | description: Text<String>, 6 | description: Text<String>,
| ^^^^^^^^^^^ | ^^^^^^^^^^^
error: Could not parse size limit `2 megabytes`: invalid digit found in string error: Could not parse size limit `2 megabytes`: couldn't parse "megabytes" into a known SI unit, Failed to parse unit "meg..."
--> tests/trybuild/size-limit-parse-fail.rs:12:5 --> tests/trybuild/size-limit-parse-fail.rs:12:5
| |
12 | description: Text<String>, 12 | description: Text<String>,
| ^^^^^^^^^^^ | ^^^^^^^^^^^
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.82.
## 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,19 +1,19 @@
[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"]
all-features = true all-features = true
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
@ -37,12 +37,11 @@ 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"
@ -50,21 +49,27 @@ 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"
tempfile = { version = "3.4", optional = true } tempfile = { version = "3.4", optional = true }
tokio = { version = "1.24.2", features = ["sync", "io-util"] } tokio = { version = "1.38.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-test = "0.3"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
multer = "3" multer = "3"
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"
[lints]
workspace = true

View File

@ -3,11 +3,11 @@
<!-- 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)
@ -15,14 +15,19 @@
<!-- cargo-rdme start --> <!-- cargo-rdme start -->
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
```rust ```rust
use actix_web::{post, App, HttpServer, Responder}; use actix_multipart::form::{
json::Json as MpJson, tempfile::TempFile, MultipartForm, MultipartFormConfig,
use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm}; };
use actix_web::{middleware::Logger, post, App, HttpServer, Responder};
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -32,37 +37,51 @@ struct Metadata {
#[derive(Debug, MultipartForm)] #[derive(Debug, MultipartForm)]
struct UploadForm { struct UploadForm {
// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
#[multipart(limit = "100MB")] #[multipart(limit = "100MB")]
file: TempFile, file: TempFile,
json: MPJson<Metadata>, json: MpJson<Metadata>,
} }
#[post("/videos")] #[post("/videos")]
pub async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder { async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
format!( format!(
"Uploaded file {}, with size: {}", "Uploaded file {}, with size: {}\ntemporary file ({}) was deleted\n",
form.json.name, form.file.size form.json.name,
form.file.size,
form.file.file.path().display(),
) )
} }
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
HttpServer::new(move || App::new().service(post_video)) env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
HttpServer::new(move || {
App::new()
.service(post_video)
.wrap(Logger::default())
// Also increase the global total limit to 100MiB.
.app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
})
.workers(2)
.bind(("127.0.0.1", 8080))? .bind(("127.0.0.1", 8080))?
.run() .run()
.await .await
} }
``` ```
<!-- cargo-rdme end --> cURL request:
[More available in the examples repo &rarr;](https://github.com/actix/examples/tree/master/forms/multipart) ```sh
Curl request :
```bash
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
``` ```
[`MultipartForm`]: struct@form::MultipartForm
<!-- cargo-rdme end -->
[More available in the examples repo &rarr;](https://github.com/actix/examples/tree/main/forms/multipart)

View File

@ -0,0 +1,45 @@
use actix_multipart::form::{
json::Json as MpJson, tempfile::TempFile, MultipartForm, MultipartFormConfig,
};
use actix_web::{middleware::Logger, post, App, HttpServer, Responder};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Metadata {
name: String,
}
#[derive(Debug, MultipartForm)]
struct UploadForm {
// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
#[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())
// Also increase the global total limit to 100MiB.
.app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
})
.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,
@ -80,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,
)))) ))))
} }
@ -95,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(())
@ -123,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();
@ -159,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,
)))) ))))
} }
@ -174,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(())
@ -281,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);
@ -294,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);
@ -313,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);
@ -329,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))
} }
@ -752,6 +770,41 @@ mod tests {
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)")] #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: Connect(Disconnected)")]
#[actix_web::test] #[actix_web::test]
async fn field_try_next_panic() { async fn field_try_next_panic() {

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,11 +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, MultipartFormConfig};
//! use serde::Deserialize; //! use serde::Deserialize;
//! //!
//! #[derive(Debug, Deserialize)] //! #[derive(Debug, Deserialize)]
@ -15,9 +23,10 @@
//! //!
//! #[derive(Debug, MultipartForm)] //! #[derive(Debug, MultipartForm)]
//! struct UploadForm { //! struct UploadForm {
//! // Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
//! #[multipart(limit = "100MB")] //! #[multipart(limit = "100MB")]
//! file: TempFile, //! file: TempFile,
//! json: MPJson<Metadata>, //! json: MpJson<Metadata>,
//! } //! }
//! //!
//! #[post("/videos")] //! #[post("/videos")]
@ -30,19 +39,32 @@
//! //!
//! #[actix_web::main] //! #[actix_web::main]
//! async fn main() -> std::io::Result<()> { //! async fn main() -> std::io::Result<()> {
//! HttpServer::new(move || App::new().service(post_video)) //! HttpServer::new(move || {
//! App::new()
//! .service(post_video)
//! // Also increase the global total limit to 100MiB.
//! .app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
//! })
//! .bind(("127.0.0.1", 8080))? //! .bind(("127.0.0.1", 8080))?
//! .run() //! .run()
//! .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_cfg))]
// This allows us to use the actix_multipart_derive within this crate's tests // This allows us to use the actix_multipart_derive within this crate's tests
#[cfg(test)] #[cfg(test)]
@ -50,14 +72,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

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

View File

@ -13,10 +13,7 @@ license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
[package.metadata.cargo_check_external_types] [package.metadata.cargo_check_external_types]
allowed_external_types = [ allowed_external_types = ["http::*", "serde::*"]
"http::*",
"serde::*",
]
[features] [features]
default = ["http", "unicode"] default = ["http", "unicode"]
@ -35,8 +32,11 @@ tracing = { version = "0.1.30", default-features = false, features = ["log"] }
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.5", features = ["html_reports"] }
http = "0.2.7" http = "0.2.7"
serde = { version = "1", features = ["derive"] }
percent-encoding = "2.1" percent-encoding = "2.1"
serde = { version = "1", features = ["derive"] }
[lints]
workspace = true
[[bench]] [[bench]]
name = "router" name = "router"

View File

@ -13,6 +13,7 @@ macro_rules! register {
register!(finish => "(.*)", "(.*)", "(.*)", "(.*)") register!(finish => "(.*)", "(.*)", "(.*)", "(.*)")
}}; }};
(finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{
#[expect(clippy::useless_concat)]
let arr = [ let arr = [
concat!("/authorizations"), concat!("/authorizations"),
concat!("/authorizations/", $p1), concat!("/authorizations/", $p1),

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,10 +1,8 @@
//! 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_cfg))]
mod de; mod de;
mod path; mod path;

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)]

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