mirror of https://github.com/fafhrd91/actix-web
Merge remote-tracking branch 'upstream/master' into hide-authorization-header-in-httprequest-debug-output
This commit is contained in:
commit
ce3d738af4
|
@ -3,34 +3,40 @@ name: Bug Report
|
||||||
about: Create a bug report.
|
about: Create a bug report.
|
||||||
---
|
---
|
||||||
|
|
||||||
Your issue may already be reported!
|
Your issue may already be reported! Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one.
|
||||||
Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one.
|
|
||||||
|
|
||||||
## Expected Behavior
|
## Expected Behavior
|
||||||
|
|
||||||
<!--- If you're describing a bug, tell us what should happen -->
|
<!--- If you're describing a bug, tell us what should happen -->
|
||||||
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
||||||
|
|
||||||
## Current Behavior
|
## Current Behavior
|
||||||
|
|
||||||
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||||
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
||||||
|
|
||||||
## Possible Solution
|
## Possible Solution
|
||||||
|
|
||||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||||
<!--- or ideas how to implement the addition or change -->
|
<!--- or ideas how to implement the addition or change -->
|
||||||
|
|
||||||
## Steps to Reproduce (for bugs)
|
## Steps to Reproduce (for bugs)
|
||||||
|
|
||||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||||
|
|
||||||
1.
|
1.
|
||||||
2.
|
2.
|
||||||
3.
|
3.
|
||||||
4.
|
4.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||||
|
|
||||||
## Your Environment
|
## Your Environment
|
||||||
|
|
||||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||||
|
|
||||||
- Rust Version (I.e, output of `rustc -V`):
|
- Rust Version (I.e, output of `rustc -V`):
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
<!-- Please fill out the following to get your PR reviewed quicker. -->
|
<!-- Please fill out the following to get your PR reviewed quicker. -->
|
||||||
|
|
||||||
## PR Type
|
## PR Type
|
||||||
|
|
||||||
<!-- What kind of change does this PR make? -->
|
<!-- What kind of change does this PR make? -->
|
||||||
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
|
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
|
||||||
|
|
||||||
PR_TYPE
|
PR_TYPE
|
||||||
|
|
||||||
|
|
||||||
## PR Checklist
|
## PR Checklist
|
||||||
|
|
||||||
<!-- Check your PR fulfills the following items. -->
|
<!-- Check your PR fulfills the following items. -->
|
||||||
<!-- For draft PRs check the boxes as you complete them. -->
|
<!-- For draft PRs check the boxes as you complete them. -->
|
||||||
|
|
||||||
|
@ -17,11 +19,10 @@ PR_TYPE
|
||||||
- [ ] Format code with the latest stable rustfmt.
|
- [ ] Format code with the latest stable rustfmt.
|
||||||
- [ ] (Team) Label with affected crates and semver status.
|
- [ ] (Team) Label with affected crates and semver status.
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
<!-- Describe the current and new behavior. -->
|
<!-- Describe the current and new behavior. -->
|
||||||
<!-- Emphasize any breaking changes. -->
|
<!-- Emphasize any breaking changes. -->
|
||||||
|
|
||||||
|
|
||||||
<!-- If this PR fixes or closes an issue, reference it here. -->
|
<!-- If this PR fixes or closes an issue, reference it here. -->
|
||||||
<!-- Closes #000 -->
|
<!-- Closes #000 -->
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"proseWrap": "never"
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
proseWrap: never
|
|
@ -5,6 +5,7 @@ members = [
|
||||||
"actix-http-test",
|
"actix-http-test",
|
||||||
"actix-http",
|
"actix-http",
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
|
"actix-multipart-derive",
|
||||||
"actix-router",
|
"actix-router",
|
||||||
"actix-test",
|
"actix-test",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
|
@ -27,6 +28,7 @@ actix-files = { path = "actix-files" }
|
||||||
actix-http = { path = "actix-http" }
|
actix-http = { path = "actix-http" }
|
||||||
actix-http-test = { path = "actix-http-test" }
|
actix-http-test = { path = "actix-http-test" }
|
||||||
actix-multipart = { path = "actix-multipart" }
|
actix-multipart = { path = "actix-multipart" }
|
||||||
|
actix-multipart-derive = { path = "actix-multipart-derive" }
|
||||||
actix-router = { path = "actix-router" }
|
actix-router = { path = "actix-router" }
|
||||||
actix-test = { path = "actix-test" }
|
actix-test = { path = "actix-test" }
|
||||||
actix-web = { path = "actix-web" }
|
actix-web = { path = "actix-web" }
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
## 0.6.3 - 2023-01-21
|
||||||
|
|
||||||
- XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903]
|
- XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||||
|
- Update `tokio-uring` dependency to `0.4`.
|
||||||
|
|
||||||
[#2903]: https://github.com/actix/actix-web/pull/2903
|
[#2903]: https://github.com/actix/actix-web/pull/2903
|
||||||
|
|
||||||
## 0.6.2 - 2022-07-23
|
## 0.6.2 - 2022-07-23
|
||||||
|
|
||||||
- Allow partial range responses for video content to start streaming sooner. [#2817]
|
- Allow partial range responses for video content to start streaming sooner. [#2817]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
[#2817]: https://github.com/actix/actix-web/pull/2817
|
[#2817]: https://github.com/actix/actix-web/pull/2817
|
||||||
|
|
||||||
|
|
||||||
## 0.6.1 - 2022-06-11
|
## 0.6.1 - 2022-06-11
|
||||||
|
|
||||||
- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021]
|
- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021]
|
||||||
- Update `tokio-uring` dependency to `0.3`.
|
- Update `tokio-uring` dependency to `0.3`.
|
||||||
- Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645]
|
- Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645]
|
||||||
|
@ -22,46 +27,46 @@
|
||||||
[#2021]: https://github.com/actix/actix-web/pull/2021
|
[#2021]: https://github.com/actix/actix-web/pull/2021
|
||||||
[#2645]: https://github.com/actix/actix-web/pull/2645
|
[#2645]: https://github.com/actix/actix-web/pull/2645
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0 - 2022-02-25
|
## 0.6.0 - 2022-02-25
|
||||||
|
|
||||||
- No significant changes since `0.6.0-beta.16`.
|
- No significant changes since `0.6.0-beta.16`.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.16 - 2022-01-31
|
## 0.6.0-beta.16 - 2022-01-31
|
||||||
|
|
||||||
- No significant changes since `0.6.0-beta.15`.
|
- No significant changes since `0.6.0-beta.15`.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.15 - 2022-01-21
|
## 0.6.0-beta.15 - 2022-01-21
|
||||||
|
|
||||||
- No significant changes since `0.6.0-beta.14`.
|
- No significant changes since `0.6.0-beta.14`.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.14 - 2022-01-14
|
## 0.6.0-beta.14 - 2022-01-14
|
||||||
|
|
||||||
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
|
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
|
||||||
|
|
||||||
[#2583]: https://github.com/actix/actix-web/pull/2583
|
[#2583]: https://github.com/actix/actix-web/pull/2583
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.13 - 2022-01-04
|
## 0.6.0-beta.13 - 2022-01-04
|
||||||
|
|
||||||
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
|
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
|
||||||
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
|
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
[#2398]: https://github.com/actix/actix-web/pull/2398
|
[#2398]: https://github.com/actix/actix-web/pull/2398
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.12 - 2021-12-29
|
## 0.6.0-beta.12 - 2021-12-29
|
||||||
|
|
||||||
- No significant changes since `0.6.0-beta.11`.
|
- No significant changes since `0.6.0-beta.11`.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.11 - 2021-12-27
|
## 0.6.0-beta.11 - 2021-12-27
|
||||||
|
|
||||||
- No significant changes since `0.6.0-beta.10`.
|
- No significant changes since `0.6.0-beta.10`.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.10 - 2021-12-11
|
## 0.6.0-beta.10 - 2021-12-11
|
||||||
|
|
||||||
- No significant changes since `0.6.0-beta.9`.
|
- No significant changes since `0.6.0-beta.9`.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.9 - 2021-11-22
|
## 0.6.0-beta.9 - 2021-11-22
|
||||||
|
|
||||||
- Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
|
- Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
|
||||||
- Add `NamedFile::open_async`. [#2408]
|
- Add `NamedFile::open_async`. [#2408]
|
||||||
- Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
|
- Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
|
||||||
|
@ -72,24 +77,24 @@
|
||||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||||
[#2453]: https://github.com/actix/actix-web/pull/2453
|
[#2453]: https://github.com/actix/actix-web/pull/2453
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.8 - 2021-10-20
|
## 0.6.0-beta.8 - 2021-10-20
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.7 - 2021-09-09
|
## 0.6.0-beta.7 - 2021-09-09
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.6 - 2021-06-26
|
## 0.6.0-beta.6 - 2021-06-26
|
||||||
|
|
||||||
- Added `Files::path_filter()`. [#2274]
|
- Added `Files::path_filter()`. [#2274]
|
||||||
- `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
|
- `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
|
||||||
|
|
||||||
[#2274]: https://github.com/actix/actix-web/pull/2274
|
[#2274]: https://github.com/actix/actix-web/pull/2274
|
||||||
[#2228]: https://github.com/actix/actix-web/pull/2228
|
[#2228]: https://github.com/actix/actix-web/pull/2228
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.5 - 2021-06-17
|
## 0.6.0-beta.5 - 2021-06-17
|
||||||
|
|
||||||
- `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
|
- `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
|
||||||
- For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
|
- For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
|
||||||
- `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
|
- `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
|
||||||
|
@ -100,58 +105,58 @@
|
||||||
[#2225]: https://github.com/actix/actix-web/pull/2225
|
[#2225]: https://github.com/actix/actix-web/pull/2225
|
||||||
[#2257]: https://github.com/actix/actix-web/pull/2257
|
[#2257]: https://github.com/actix/actix-web/pull/2257
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.4 - 2021-04-02
|
## 0.6.0-beta.4 - 2021-04-02
|
||||||
|
|
||||||
- Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
|
- Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
|
||||||
|
|
||||||
[#2046]: https://github.com/actix/actix-web/pull/2046
|
[#2046]: https://github.com/actix/actix-web/pull/2046
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.3 - 2021-03-09
|
## 0.6.0-beta.3 - 2021-03-09
|
||||||
|
|
||||||
- No notable changes.
|
- No notable changes.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.2 - 2021-02-10
|
## 0.6.0-beta.2 - 2021-02-10
|
||||||
|
|
||||||
- Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
|
- Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
|
||||||
- Replace `v_htmlescape` with `askama_escape`. [#1953]
|
- Replace `v_htmlescape` with `askama_escape`. [#1953]
|
||||||
|
|
||||||
[#1887]: https://github.com/actix/actix-web/pull/1887
|
[#1887]: https://github.com/actix/actix-web/pull/1887
|
||||||
[#1953]: https://github.com/actix/actix-web/pull/1953
|
[#1953]: https://github.com/actix/actix-web/pull/1953
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.1 - 2021-01-07
|
## 0.6.0-beta.1 - 2021-01-07
|
||||||
|
|
||||||
- `HttpRange::parse` now has its own error type.
|
- `HttpRange::parse` now has its own error type.
|
||||||
- Update `bytes` to `1.0`. [#1813]
|
- Update `bytes` to `1.0`. [#1813]
|
||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0 - 2020-12-26
|
## 0.5.0 - 2020-12-26
|
||||||
|
|
||||||
- Optionally support hidden files/directories. [#1811]
|
- Optionally support hidden files/directories. [#1811]
|
||||||
|
|
||||||
[#1811]: https://github.com/actix/actix-web/pull/1811
|
[#1811]: https://github.com/actix/actix-web/pull/1811
|
||||||
|
|
||||||
|
|
||||||
## 0.4.1 - 2020-11-24
|
## 0.4.1 - 2020-11-24
|
||||||
|
|
||||||
- Clarify order of parameters in `Files::new` and improve docs.
|
- Clarify order of parameters in `Files::new` and improve docs.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0 - 2020-10-06
|
## 0.4.0 - 2020-10-06
|
||||||
|
|
||||||
- Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
|
- Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
|
||||||
|
|
||||||
[#1714]: https://github.com/actix/actix-web/pull/1714
|
[#1714]: https://github.com/actix/actix-web/pull/1714
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0 - 2020-09-11
|
## 0.3.0 - 2020-09-11
|
||||||
|
|
||||||
- No significant changes from 0.3.0-beta.1.
|
- No significant changes from 0.3.0-beta.1.
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0-beta.1 - 2020-07-15
|
## 0.3.0-beta.1 - 2020-07-15
|
||||||
|
|
||||||
- Update `v_htmlescape` to 0.10
|
- Update `v_htmlescape` to 0.10
|
||||||
- Update `actix-web` and `actix-http` dependencies to beta.1
|
- Update `actix-web` and `actix-http` dependencies to beta.1
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0-alpha.1 - 2020-05-23
|
## 0.3.0-alpha.1 - 2020-05-23
|
||||||
|
|
||||||
- Update `actix-web` and `actix-http` dependencies to alpha
|
- Update `actix-web` and `actix-http` dependencies to alpha
|
||||||
- Fix some typos in the docs
|
- Fix some typos in the docs
|
||||||
- Bump minimum supported Rust version to 1.40
|
- Bump minimum supported Rust version to 1.40
|
||||||
|
@ -159,73 +164,73 @@
|
||||||
|
|
||||||
[#1384]: https://github.com/actix/actix-web/pull/1384
|
[#1384]: https://github.com/actix/actix-web/pull/1384
|
||||||
|
|
||||||
|
|
||||||
## 0.2.1 - 2019-12-22
|
## 0.2.1 - 2019-12-22
|
||||||
|
|
||||||
- Use the same format for file URLs regardless of platforms
|
- Use the same format for file URLs regardless of platforms
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0 - 2019-12-20
|
## 0.2.0 - 2019-12-20
|
||||||
|
|
||||||
- Fix BodyEncoding trait import #1220
|
- Fix BodyEncoding trait import #1220
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0-alpha.1 - 2019-12-07
|
## 0.2.0-alpha.1 - 2019-12-07
|
||||||
|
|
||||||
- Migrate to `std::future`
|
- Migrate to `std::future`
|
||||||
|
|
||||||
|
|
||||||
## 0.1.7 - 2019-11-06
|
## 0.1.7 - 2019-11-06
|
||||||
- Add an additional `filename*` param in the `Content-Disposition` header of
|
|
||||||
`actix_files::NamedFile` to be more compatible. (#1151)
|
- Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
|
||||||
|
|
||||||
## 0.1.6 - 2019-10-14
|
## 0.1.6 - 2019-10-14
|
||||||
|
|
||||||
- Add option to redirect to a slash-ended path `Files` #1132
|
- Add option to redirect to a slash-ended path `Files` #1132
|
||||||
|
|
||||||
|
|
||||||
## 0.1.5 - 2019-10-08
|
## 0.1.5 - 2019-10-08
|
||||||
|
|
||||||
- Bump up `mime_guess` crate version to 2.0.1
|
- Bump up `mime_guess` crate version to 2.0.1
|
||||||
- Bump up `percent-encoding` crate version to 2.1
|
- Bump up `percent-encoding` crate version to 2.1
|
||||||
- Allow user defined request guards for `Files` #1113
|
- Allow user defined request guards for `Files` #1113
|
||||||
|
|
||||||
|
|
||||||
## 0.1.4 - 2019-07-20
|
## 0.1.4 - 2019-07-20
|
||||||
|
|
||||||
- Allow to disable `Content-Disposition` header #686
|
- Allow to disable `Content-Disposition` header #686
|
||||||
|
|
||||||
|
|
||||||
## 0.1.3 - 2019-06-28
|
## 0.1.3 - 2019-06-28
|
||||||
|
|
||||||
- Do not set `Content-Length` header, let actix-http set it #930
|
- Do not set `Content-Length` header, let actix-http set it #930
|
||||||
|
|
||||||
|
|
||||||
## 0.1.2 - 2019-06-13
|
## 0.1.2 - 2019-06-13
|
||||||
|
|
||||||
- Content-Length is 0 for NamedFile HEAD request #914
|
- Content-Length is 0 for NamedFile HEAD request #914
|
||||||
- Fix ring dependency from actix-web default features for #741
|
- Fix ring dependency from actix-web default features for #741
|
||||||
|
|
||||||
|
|
||||||
## 0.1.1 - 2019-06-01
|
## 0.1.1 - 2019-06-01
|
||||||
|
|
||||||
- Static files are incorrectly served as both chunked and with length #812
|
- Static files are incorrectly served as both chunked and with length #812
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0 - 2019-05-25
|
## 0.1.0 - 2019-05-25
|
||||||
|
|
||||||
- NamedFile last-modified check always fails due to nano-seconds in file modified date #820
|
- NamedFile last-modified check always fails due to nano-seconds in file modified date #820
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.4 - 2019-05-12
|
## 0.1.0-beta.4 - 2019-05-12
|
||||||
|
|
||||||
- Update actix-web to beta.4
|
- Update actix-web to beta.4
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.1 - 2019-04-20
|
## 0.1.0-beta.1 - 2019-04-20
|
||||||
|
|
||||||
- Update actix-web to beta.1
|
- Update actix-web to beta.1
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.6 - 2019-04-14
|
## 0.1.0-alpha.6 - 2019-04-14
|
||||||
|
|
||||||
- Update actix-web to alpha6
|
- Update actix-web to alpha6
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.4 - 2019-04-08
|
## 0.1.0-alpha.4 - 2019-04-08
|
||||||
|
|
||||||
- Update actix-web to alpha4
|
- Update actix-web to alpha4
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.2 - 2019-04-02
|
## 0.1.0-alpha.2 - 2019-04-02
|
||||||
|
|
||||||
- Add default handler support
|
- Add default handler support
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.1 - 2019-03-28
|
## 0.1.0-alpha.1 - 2019-03-28
|
||||||
|
|
||||||
- Initial impl
|
- Initial impl
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-files"
|
name = "actix-files"
|
||||||
version = "0.6.2"
|
version = "0.6.3"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
@ -40,8 +40,8 @@ v_htmlescape= "0.15"
|
||||||
|
|
||||||
# experimental-io-uring
|
# experimental-io-uring
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
tokio-uring = { version = "0.3", optional = true, features = ["bytes"] }
|
tokio-uring = { version = "0.4", optional = true, features = ["bytes"] }
|
||||||
actix-server = { version = "2.1", optional = true } # ensure matching tokio-uring versions
|
actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.7"
|
actix-rt = "2.7"
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
> Static file serving for Actix Web
|
> Static file serving for Actix Web
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://docs.rs/actix-files/0.6.2)
|
[](https://docs.rs/actix-files/0.6.3)
|
||||||

|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-files/0.6.2)
|
[](https://deps.rs/crate/actix-files/0.6.3)
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@ -15,4 +15,4 @@
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-files)
|
- [API Documentation](https://docs.rs/actix-files)
|
||||||
- [Example Project](https://github.com/actix/examples/tree/master/basics/static-files)
|
- [Example Project](https://github.com/actix/examples/tree/master/basics/static-files)
|
||||||
- Minimum Supported Rust Version (MSRV): 1.54
|
- Minimum Supported Rust Version (MSRV): 1.59
|
||||||
|
|
|
@ -142,7 +142,7 @@ impl Files {
|
||||||
self
|
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
|
||||||
for<'r, 's> F:
|
for<'r, 's> F:
|
||||||
|
@ -152,7 +152,7 @@ impl Files {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specifies mime override callback
|
/// Specifies MIME override callback.
|
||||||
pub fn mime_override<F>(mut self, f: F) -> Self
|
pub fn mime_override<F>(mut self, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
|
F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
|
||||||
|
@ -390,3 +390,42 @@ impl ServiceFactory<ServiceRequest> for Files {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use actix_web::{
|
||||||
|
http::StatusCode,
|
||||||
|
test::{self, TestRequest},
|
||||||
|
App, HttpResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn custom_files_listing_renderer() {
|
||||||
|
let srv = test::init_service(
|
||||||
|
App::new().service(
|
||||||
|
Files::new("/", "./tests")
|
||||||
|
.show_files_listing()
|
||||||
|
.files_listing_renderer(|dir, req| {
|
||||||
|
Ok(ServiceResponse::new(
|
||||||
|
req.clone(),
|
||||||
|
HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()),
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
let body = test::read_body(res).await;
|
||||||
|
assert!(
|
||||||
|
body.ends_with(b"actix-files/tests/"),
|
||||||
|
"body {:?} does not end with `actix-files/tests/`",
|
||||||
|
body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
|
|
|
@ -30,7 +30,7 @@ impl PathBufWrap {
|
||||||
let mut segment_count = path.matches('/').count() + 1;
|
let mut segment_count = path.matches('/').count() + 1;
|
||||||
|
|
||||||
// we can decode the whole path here (instead of per-segment decoding)
|
// we can decode the whole path here (instead of per-segment decoding)
|
||||||
// because we will reject `%2F` in paths using `segement_count`.
|
// because we will reject `%2F` in paths using `segment_count`.
|
||||||
let path = percent_encoding::percent_decode_str(path)
|
let path = percent_encoding::percent_decode_str(path)
|
||||||
.decode_utf8()
|
.decode_utf8()
|
||||||
.map_err(|_| UriSegmentError::NotValidUtf8)?;
|
.map_err(|_| UriSegmentError::NotValidUtf8)?;
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
## 3.1.0 - 2023-01-21
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59.
|
- Minimum supported Rust version (MSRV) is now 1.59.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0 - 2022-07-24
|
## 3.0.0 - 2022-07-24
|
||||||
|
|
||||||
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
||||||
- Added `TestServer::client_headers` method. [#2097]
|
- Added `TestServer::client_headers` method. [#2097]
|
||||||
- Update `actix-server` dependency to `2`.
|
- Update `actix-server` dependency to `2`.
|
||||||
|
@ -16,71 +19,71 @@
|
||||||
[#2097]: https://github.com/actix/actix-web/pull/2097
|
[#2097]: https://github.com/actix/actix-web/pull/2097
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>3.0.0 Pre-Releases</summary>
|
<summary>3.0.0 Pre-Releases</summary>
|
||||||
|
|
||||||
## 3.0.0-beta.13 - 2022-02-16
|
## 3.0.0-beta.13 - 2022-02-16
|
||||||
|
|
||||||
- No significant changes since `3.0.0-beta.12`.
|
- No significant changes since `3.0.0-beta.12`.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.12 - 2022-01-31
|
## 3.0.0-beta.12 - 2022-01-31
|
||||||
|
|
||||||
- No significant changes since `3.0.0-beta.11`.
|
- No significant changes since `3.0.0-beta.11`.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.11 - 2022-01-04
|
## 3.0.0-beta.11 - 2022-01-04
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.10 - 2021-12-27
|
## 3.0.0-beta.10 - 2021-12-27
|
||||||
|
|
||||||
- Update `actix-server` to `2.0.0-rc.2`. [#2550]
|
- Update `actix-server` to `2.0.0-rc.2`. [#2550]
|
||||||
|
|
||||||
[#2550]: https://github.com/actix/actix-web/pull/2550
|
[#2550]: https://github.com/actix/actix-web/pull/2550
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.9 - 2021-12-11
|
## 3.0.0-beta.9 - 2021-12-11
|
||||||
|
|
||||||
- No significant changes since `3.0.0-beta.8`.
|
- No significant changes since `3.0.0-beta.8`.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.8 - 2021-11-30
|
## 3.0.0-beta.8 - 2021-11-30
|
||||||
|
|
||||||
- Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
- Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||||
|
|
||||||
[#2474]: https://github.com/actix/actix-web/pull/2474
|
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.7 - 2021-11-22
|
## 3.0.0-beta.7 - 2021-11-22
|
||||||
|
|
||||||
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||||
|
|
||||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.6 - 2021-11-15
|
## 3.0.0-beta.6 - 2021-11-15
|
||||||
|
|
||||||
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
||||||
- Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
- Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.5 - 2021-09-09
|
## 3.0.0-beta.5 - 2021-09-09
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.4 - 2021-04-02
|
## 3.0.0-beta.4 - 2021-04-02
|
||||||
|
|
||||||
- Added `TestServer::client_headers` method. [#2097]
|
- Added `TestServer::client_headers` method. [#2097]
|
||||||
|
|
||||||
[#2097]: https://github.com/actix/actix-web/pull/2097
|
[#2097]: https://github.com/actix/actix-web/pull/2097
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.3 - 2021-03-09
|
## 3.0.0-beta.3 - 2021-03-09
|
||||||
- No notable changes.
|
|
||||||
|
|
||||||
|
- No notable changes.
|
||||||
|
|
||||||
## 3.0.0-beta.2 - 2021-02-10
|
## 3.0.0-beta.2 - 2021-02-10
|
||||||
|
|
||||||
- No notable changes.
|
- No notable changes.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.1 - 2021-01-07
|
## 3.0.0-beta.1 - 2021-01-07
|
||||||
|
|
||||||
- Update `bytes` to `1.0`. [#1813]
|
- Update `bytes` to `1.0`. [#1813]
|
||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
@ -88,6 +91,7 @@
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## 2.1.0 - 2020-11-25
|
## 2.1.0 - 2020-11-25
|
||||||
|
|
||||||
- Add ability to set address for `TestServer`. [#1645]
|
- Add ability to set address for `TestServer`. [#1645]
|
||||||
- Upgrade `base64` to `0.13`.
|
- Upgrade `base64` to `0.13`.
|
||||||
- Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
- Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||||
|
@ -95,12 +99,12 @@
|
||||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||||
[#1645]: https://github.com/actix/actix-web/pull/1645
|
[#1645]: https://github.com/actix/actix-web/pull/1645
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0 - 2020-09-11
|
## 2.0.0 - 2020-09-11
|
||||||
|
|
||||||
- Update actix-codec and actix-utils dependencies.
|
- Update actix-codec and actix-utils dependencies.
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0-alpha.1 - 2020-05-23
|
## 2.0.0-alpha.1 - 2020-05-23
|
||||||
|
|
||||||
- Update the `time` dependency to 0.2.7
|
- Update the `time` dependency to 0.2.7
|
||||||
- Update `actix-connect` dependency to 2.0.0-alpha.2
|
- Update `actix-connect` dependency to 2.0.0-alpha.2
|
||||||
- Make `test_server` `async` fn.
|
- Make `test_server` `async` fn.
|
||||||
|
@ -110,55 +114,56 @@
|
||||||
- Update `env_logger` dependency to 0.7
|
- Update `env_logger` dependency to 0.7
|
||||||
|
|
||||||
## 1.0.0 - 2019-12-13
|
## 1.0.0 - 2019-12-13
|
||||||
|
|
||||||
- Replaced `TestServer::start()` with `test_server()`
|
- Replaced `TestServer::start()` with `test_server()`
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0-alpha.3 - 2019-12-07
|
## 1.0.0-alpha.3 - 2019-12-07
|
||||||
|
|
||||||
- Migrate to `std::future`
|
- Migrate to `std::future`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.5 - 2019-09-17
|
## 0.2.5 - 2019-09-17
|
||||||
|
|
||||||
- Update serde_urlencoded to "0.6.1"
|
- Update serde_urlencoded to "0.6.1"
|
||||||
- Increase TestServerRuntime timeouts from 500ms to 3000ms
|
- Increase TestServerRuntime timeouts from 500ms to 3000ms
|
||||||
- Do not override current `System`
|
- Do not override current `System`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.4 - 2019-07-18
|
## 0.2.4 - 2019-07-18
|
||||||
|
|
||||||
- Update actix-server to 0.6
|
- Update actix-server to 0.6
|
||||||
|
|
||||||
|
|
||||||
## 0.2.3 - 2019-07-16
|
## 0.2.3 - 2019-07-16
|
||||||
|
|
||||||
- Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
- Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.2 - 2019-06-16
|
## 0.2.2 - 2019-06-16
|
||||||
|
|
||||||
- Add .put() and .sput() methods
|
- Add .put() and .sput() methods
|
||||||
|
|
||||||
|
|
||||||
## 0.2.1 - 2019-06-05
|
## 0.2.1 - 2019-06-05
|
||||||
|
|
||||||
- Add license files
|
- Add license files
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0 - 2019-05-12
|
## 0.2.0 - 2019-05-12
|
||||||
|
|
||||||
- Update awc and actix-http deps
|
- Update awc and actix-http deps
|
||||||
|
|
||||||
|
|
||||||
## 0.1.1 - 2019-04-24
|
## 0.1.1 - 2019-04-24
|
||||||
|
|
||||||
- Always make new connection for http client
|
- Always make new connection for http client
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0 - 2019-04-16
|
## 0.1.0 - 2019-04-16
|
||||||
|
|
||||||
- No changes
|
- No changes
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.3 - 2019-04-02
|
## 0.1.0-alpha.3 - 2019-04-02
|
||||||
|
|
||||||
- Request functions accept path #743
|
- Request functions accept path #743
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.2 - 2019-03-29
|
## 0.1.0-alpha.2 - 2019-03-29
|
||||||
|
|
||||||
- Added TestServerRuntime::load_body() method
|
- Added TestServerRuntime::load_body() method
|
||||||
- Update actix-http and awc libraries
|
- Update actix-http and awc libraries
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.1 - 2019-03-28
|
## 0.1.0-alpha.1 - 2019-03-28
|
||||||
|
|
||||||
- Initial impl
|
- Initial impl
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-http-test"
|
name = "actix-http-test"
|
||||||
version = "3.0.0"
|
version = "3.1.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Various helpers for Actix applications to use during testing"
|
description = "Various helpers for Actix applications to use during testing"
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
|
@ -37,7 +37,6 @@ actix-rt = "2.2"
|
||||||
actix-server = "2"
|
actix-server = "2"
|
||||||
awc = { version = "3", default-features = false }
|
awc = { version = "3", default-features = false }
|
||||||
|
|
||||||
base64 = "0.13"
|
|
||||||
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.5"
|
http = "0.2.5"
|
||||||
|
@ -48,7 +47,7 @@ serde_json = "1.0"
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||||
tokio = { version = "1.8.4", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
> Various helpers for Actix applications to use during testing.
|
> Various helpers for Actix applications to use during testing.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://docs.rs/actix-http-test/3.0.0)
|
[](https://docs.rs/actix-http-test/3.1.0)
|
||||||

|

|
||||||

|

|
||||||
<br>
|
<br>
|
||||||
[](https://deps.rs/crate/actix-http-test/3.0.0)
|
[](https://deps.rs/crate/actix-http-test/3.1.0)
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-http-test)
|
- [API Documentation](https://docs.rs/actix-http-test)
|
||||||
- Minimum Supported Rust Version (MSRV): 1.54
|
- Minimum Supported Rust Version (MSRV): 1.59
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
#![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")]
|
||||||
|
|
||||||
|
@ -87,6 +88,7 @@ pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
|
||||||
|
|
||||||
// notify TestServer that server and system have shut down
|
// notify TestServer that server and system have shut down
|
||||||
// all thread managed resources should be dropped at this point
|
// all thread managed resources should be dropped at this point
|
||||||
|
#[allow(clippy::let_underscore_future)]
|
||||||
let _ = thread_stop_tx.send(());
|
let _ = thread_stop_tx.send(());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -294,6 +296,7 @@ impl Drop for TestServer {
|
||||||
// without needing to await anything
|
// without needing to await anything
|
||||||
|
|
||||||
// signal server to stop
|
// signal server to stop
|
||||||
|
#[allow(clippy::let_underscore_future)]
|
||||||
let _ = self.server.stop(true);
|
let _ = self.server.stop(true);
|
||||||
|
|
||||||
// signal system to stop
|
// signal system to stop
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.2.2"
|
version = "3.3.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
@ -61,7 +61,7 @@ actix-codec = "0.5"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-rt = { version = "2.2", default-features = false }
|
actix-rt = { version = "2.2", default-features = false }
|
||||||
|
|
||||||
ahash = "0.7"
|
ahash = "0.8"
|
||||||
bitflags = "1.2"
|
bitflags = "1.2"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
|
@ -77,7 +77,7 @@ mime = "0.3"
|
||||||
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.13.1", features = [] }
|
tokio = { version = "1.24.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"] }
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ h2 = { version = "0.3.9", optional = true }
|
||||||
|
|
||||||
# websockets
|
# websockets
|
||||||
local-channel = { version = "0.1", optional = true }
|
local-channel = { version = "0.1", optional = true }
|
||||||
base64 = { version = "0.13", optional = true }
|
base64 = { version = "0.21", optional = true }
|
||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
sha1 = { version = "0.10", optional = true }
|
sha1 = { version = "0.10", optional = true }
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ serde_json = "1.0"
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||||
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
tokio = { version = "1.24.2", features = ["net", "rt", "macros"] }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "ws"
|
name = "ws"
|
||||||
|
|
|
@ -3,18 +3,18 @@
|
||||||
> HTTP primitives for the Actix ecosystem.
|
> HTTP primitives for the Actix ecosystem.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://docs.rs/actix-http/3.2.2)
|
[](https://docs.rs/actix-http/3.3.0)
|
||||||

|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-http/3.2.2)
|
[](https://deps.rs/crate/actix-http/3.3.0)
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-http)
|
- [API Documentation](https://docs.rs/actix-http)
|
||||||
- Minimum Supported Rust Version (MSRV): 1.54
|
- Minimum Supported Rust Version (MSRV): 1.59
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
@ -49,18 +49,3 @@ async fn main() -> io::Result<()> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under either of
|
|
||||||
|
|
||||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
|
||||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
|
||||||
|
|
||||||
at your option.
|
|
||||||
|
|
||||||
## Code of Conduct
|
|
||||||
|
|
||||||
Contribution to the actix-http crate is organized under the terms of the
|
|
||||||
Contributor Covenant, the maintainer of actix-http, @fafhrd91, promises to
|
|
||||||
intervene to uphold that code of conduct.
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||||
|
|
||||||
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
//! An example that supports automatic selection of plaintext h1/h2c connections.
|
||||||
|
//!
|
||||||
|
//! Notably, both the following commands will work.
|
||||||
|
//! ```console
|
||||||
|
//! $ curl --http1.1 'http://localhost:8080/'
|
||||||
|
//! $ curl --http2-prior-knowledge 'http://localhost:8080/'
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::{convert::Infallible, io};
|
||||||
|
|
||||||
|
use actix_http::{HttpService, Request, Response, StatusCode};
|
||||||
|
use actix_server::Server;
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
|
Server::build()
|
||||||
|
.bind("h2c-detect", ("127.0.0.1", 8080), || {
|
||||||
|
HttpService::build()
|
||||||
|
.finish(|_req: Request| async move {
|
||||||
|
Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!"))
|
||||||
|
})
|
||||||
|
.tcp_auto_h2c()
|
||||||
|
})?
|
||||||
|
.workers(2)
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
|
@ -120,7 +120,7 @@ pub trait MessageBody {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod foreign_impls {
|
mod foreign_impls {
|
||||||
use std::ops::DerefMut;
|
use std::{borrow::Cow, ops::DerefMut};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -324,6 +324,39 @@ mod foreign_impls {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MessageBody for Cow<'static, [u8]> {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let bytes = match mem::take(self.get_mut()) {
|
||||||
|
Cow::Borrowed(b) => Bytes::from_static(b),
|
||||||
|
Cow::Owned(b) => Bytes::from(b),
|
||||||
|
};
|
||||||
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
match self {
|
||||||
|
Cow::Borrowed(b) => Ok(Bytes::from_static(b)),
|
||||||
|
Cow::Owned(b) => Ok(Bytes::from(b)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MessageBody for &'static str {
|
impl MessageBody for &'static str {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
|
@ -379,6 +412,39 @@ mod foreign_impls {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MessageBody for Cow<'static, str> {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let bytes = match mem::take(self.get_mut()) {
|
||||||
|
Cow::Borrowed(s) => Bytes::from_static(s.as_bytes()),
|
||||||
|
Cow::Owned(s) => Bytes::from(s.into_bytes()),
|
||||||
|
};
|
||||||
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
match self {
|
||||||
|
Cow::Borrowed(s) => Ok(Bytes::from_static(s.as_bytes())),
|
||||||
|
Cow::Owned(s) => Ok(Bytes::from(s.into_bytes())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MessageBody for bytestring::ByteString {
|
impl MessageBody for bytestring::ByteString {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ where
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
BodySize::Sized(self.size as u64)
|
BodySize::Sized(self.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to pull out the next value of the underlying [`Stream`].
|
/// Attempts to pull out the next value of the underlying [`Stream`].
|
||||||
|
|
|
@ -186,7 +186,7 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol.
|
/// Finish service configuration and create a service for the HTTP/1 protocol.
|
||||||
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
|
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
|
@ -209,7 +209,7 @@ where
|
||||||
.on_connect_ext(self.on_connect_ext)
|
.on_connect_ext(self.on_connect_ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
/// Finish service configuration and create a service for the HTTP/2 protocol.
|
||||||
#[cfg(feature = "http2")]
|
#[cfg(feature = "http2")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||||
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||||
|
|
|
@ -71,7 +71,7 @@ impl ChunkedState {
|
||||||
|
|
||||||
match size.checked_mul(radix) {
|
match size.checked_mul(radix) {
|
||||||
Some(n) => {
|
Some(n) => {
|
||||||
*size = n as u64;
|
*size = n;
|
||||||
*size += rem as u64;
|
*size += rem as u64;
|
||||||
|
|
||||||
Poll::Ready(Ok(ChunkedState::Size))
|
Poll::Ready(Ok(ChunkedState::Size))
|
||||||
|
|
|
@ -932,7 +932,6 @@ fn http_msg(msg: impl AsRef<str>) -> BytesMut {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.into_iter()
|
|
||||||
.map(|line| [line.trim_start(), "\r"].concat())
|
.map(|line| [line.trim_start(), "\r"].concat())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
|
@ -450,7 +450,7 @@ impl TransferEncoding {
|
||||||
|
|
||||||
buf.extend_from_slice(&msg[..len as usize]);
|
buf.extend_from_slice(&msg[..len as usize]);
|
||||||
|
|
||||||
*remaining -= len as u64;
|
*remaining -= len;
|
||||||
Ok(*remaining == 0)
|
Ok(*remaining == 0)
|
||||||
} else {
|
} else {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|
|
@ -29,7 +29,7 @@ use crate::{
|
||||||
HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE,
|
HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE,
|
||||||
},
|
},
|
||||||
service::HttpFlow,
|
service::HttpFlow,
|
||||||
Extensions, OnConnectData, Payload, Request, Response, ResponseHead,
|
Extensions, Method, OnConnectData, Payload, Request, Response, ResponseHead,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CHUNK_SIZE: usize = 16_384;
|
const CHUNK_SIZE: usize = 16_384;
|
||||||
|
@ -118,6 +118,7 @@ where
|
||||||
let payload = crate::h2::Payload::new(body);
|
let payload = crate::h2::Payload::new(body);
|
||||||
let pl = Payload::H2 { payload };
|
let pl = Payload::H2 { payload };
|
||||||
let mut req = Request::with_payload(pl);
|
let mut req = Request::with_payload(pl);
|
||||||
|
let head_req = parts.method == Method::HEAD;
|
||||||
|
|
||||||
let head = req.head_mut();
|
let head = req.head_mut();
|
||||||
head.uri = parts.uri;
|
head.uri = parts.uri;
|
||||||
|
@ -135,10 +136,10 @@ where
|
||||||
actix_rt::spawn(async move {
|
actix_rt::spawn(async move {
|
||||||
// resolve service call and send response.
|
// resolve service call and send response.
|
||||||
let res = match fut.await {
|
let res = match fut.await {
|
||||||
Ok(res) => handle_response(res.into(), tx, config).await,
|
Ok(res) => handle_response(res.into(), tx, config, head_req).await,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let res: Response<BoxBody> = err.into();
|
let res: Response<BoxBody> = err.into();
|
||||||
handle_response(res, tx, config).await
|
handle_response(res, tx, config, head_req).await
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -206,6 +207,7 @@ async fn handle_response<B>(
|
||||||
res: Response<B>,
|
res: Response<B>,
|
||||||
mut tx: SendResponse<Bytes>,
|
mut tx: SendResponse<Bytes>,
|
||||||
config: ServiceConfig,
|
config: ServiceConfig,
|
||||||
|
head_req: bool,
|
||||||
) -> Result<(), DispatchError>
|
) -> Result<(), DispatchError>
|
||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
|
@ -215,14 +217,14 @@ where
|
||||||
// prepare response.
|
// prepare response.
|
||||||
let mut size = body.size();
|
let mut size = body.size();
|
||||||
let res = prepare_response(config, res.head(), &mut size);
|
let res = prepare_response(config, res.head(), &mut size);
|
||||||
let eof = size.is_eof();
|
let eof_or_head = size.is_eof() || head_req;
|
||||||
|
|
||||||
// send response head and return on eof.
|
// send response head and return on eof.
|
||||||
let mut stream = tx
|
let mut stream = tx
|
||||||
.send_response(res, eof)
|
.send_response(res, eof_or_head)
|
||||||
.map_err(DispatchError::SendResponse)?;
|
.map_err(DispatchError::SendResponse)?;
|
||||||
|
|
||||||
if eof {
|
if eof_or_head {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
//! Common header names not defined in [`http`].
|
||||||
|
//!
|
||||||
|
//! Any headers added to this file will need to be re-exported from the list at `crate::headers`.
|
||||||
|
|
||||||
|
use http::header::HeaderName;
|
||||||
|
|
||||||
|
/// Response header field that indicates how caches have handled that response and its corresponding
|
||||||
|
/// request.
|
||||||
|
///
|
||||||
|
/// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics.
|
||||||
|
// TODO(breaking): replace with http's version
|
||||||
|
pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status");
|
||||||
|
|
||||||
|
/// Response header field that allows origin servers to control the behavior of CDN caches
|
||||||
|
/// interposed between them and clients separately from other caches that might handle the response.
|
||||||
|
///
|
||||||
|
/// See [RFC 9213](https://www.rfc-editor.org/rfc/rfc9213) for full semantics.
|
||||||
|
// TODO(breaking): replace with http's version
|
||||||
|
pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control");
|
||||||
|
|
||||||
|
/// Response header that prevents a document from loading any cross-origin resources that don't
|
||||||
|
/// explicitly grant the document permission (using [CORP] or [CORS]).
|
||||||
|
///
|
||||||
|
/// [CORP]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy_(CORP)
|
||||||
|
/// [CORS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||||
|
pub const CROSS_ORIGIN_EMBEDDER_POLICY: HeaderName =
|
||||||
|
HeaderName::from_static("cross-origin-embedder-policy");
|
||||||
|
|
||||||
|
/// Response header that allows you to ensure a top-level document does not share a browsing context
|
||||||
|
/// group with cross-origin documents.
|
||||||
|
pub const CROSS_ORIGIN_OPENER_POLICY: HeaderName =
|
||||||
|
HeaderName::from_static("cross-origin-opener-policy");
|
||||||
|
|
||||||
|
/// Response header that conveys a desire that the browser blocks no-cors cross-origin/cross-site
|
||||||
|
/// requests to the given resource.
|
||||||
|
pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName =
|
||||||
|
HeaderName::from_static("cross-origin-resource-policy");
|
||||||
|
|
||||||
|
/// Response header that provides a mechanism to allow and deny the use of browser features in a
|
||||||
|
/// document or within any `<iframe>` elements in the document.
|
||||||
|
pub const PERMISSIONS_POLICY: HeaderName = HeaderName::from_static("permissions-policy");
|
||||||
|
|
||||||
|
/// Request header (de-facto standard) for identifying the originating IP address of a client
|
||||||
|
/// connecting to a web server through a proxy server.
|
||||||
|
pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
|
||||||
|
|
||||||
|
/// Request header (de-facto standard) for identifying the original host requested by the client in
|
||||||
|
/// the `Host` HTTP request header.
|
||||||
|
pub const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
|
||||||
|
|
||||||
|
/// Request header (de-facto standard) for identifying the protocol that a client used to connect to
|
||||||
|
/// your proxy or load balancer.
|
||||||
|
pub const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
|
|
@ -150,9 +150,7 @@ impl HeaderMap {
|
||||||
/// assert_eq!(map.len(), 3);
|
/// assert_eq!(map.len(), 3);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.inner
|
self.inner.values().map(|vals| vals.len()).sum()
|
||||||
.iter()
|
|
||||||
.fold(0, |acc, (_, values)| acc + values.len())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of _keys_ stored in the map.
|
/// Returns the number of _keys_ stored in the map.
|
||||||
|
@ -552,6 +550,39 @@ impl HeaderMap {
|
||||||
Keys(self.inner.keys())
|
Keys(self.inner.keys())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retains only the headers specified by the predicate.
|
||||||
|
///
|
||||||
|
/// In other words, removes all headers `(name, val)` for which `retain_fn(&name, &mut val)`
|
||||||
|
/// returns false.
|
||||||
|
///
|
||||||
|
/// The order in which headers are visited should be considered arbitrary.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
|
/// let mut map = HeaderMap::new();
|
||||||
|
///
|
||||||
|
/// map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
||||||
|
/// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1"));
|
||||||
|
/// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2"));
|
||||||
|
///
|
||||||
|
/// map.retain(|name, val| val.as_bytes().starts_with(b"one"));
|
||||||
|
///
|
||||||
|
/// assert_eq!(map.len(), 1);
|
||||||
|
/// assert!(map.contains_key(&header::SET_COOKIE));
|
||||||
|
/// ```
|
||||||
|
pub fn retain<F>(&mut self, mut retain_fn: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&HeaderName, &mut HeaderValue) -> bool,
|
||||||
|
{
|
||||||
|
self.inner.retain(|name, vals| {
|
||||||
|
vals.inner.retain(|val| retain_fn(name, val));
|
||||||
|
|
||||||
|
// invariant: make sure newly empty value lists are removed
|
||||||
|
!vals.is_empty()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Clears the map, returning all name-value sets as an iterator.
|
/// Clears the map, returning all name-value sets as an iterator.
|
||||||
///
|
///
|
||||||
/// Header names will only be yielded for the first value in each set. All items that are
|
/// Header names will only be yielded for the first value in each set. All items that are
|
||||||
|
@ -943,6 +974,55 @@ mod tests {
|
||||||
assert!(map.is_empty());
|
assert!(map.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retain() {
|
||||||
|
let mut map = HeaderMap::new();
|
||||||
|
|
||||||
|
map.append(header::LOCATION, HeaderValue::from_static("/test"));
|
||||||
|
map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
||||||
|
map.append(header::COOKIE, HeaderValue::from_static("one=1"));
|
||||||
|
map.append(header::COOKIE, HeaderValue::from_static("two=2"));
|
||||||
|
|
||||||
|
assert_eq!(map.len(), 4);
|
||||||
|
|
||||||
|
// by value
|
||||||
|
map.retain(|_, val| !val.as_bytes().contains(&b'/'));
|
||||||
|
assert_eq!(map.len(), 3);
|
||||||
|
|
||||||
|
// by name
|
||||||
|
map.retain(|name, _| name.as_str() != "cookie");
|
||||||
|
assert_eq!(map.len(), 1);
|
||||||
|
|
||||||
|
// keep but mutate value
|
||||||
|
map.retain(|_, val| {
|
||||||
|
*val = HeaderValue::from_static("replaced");
|
||||||
|
true
|
||||||
|
});
|
||||||
|
assert_eq!(map.len(), 1);
|
||||||
|
assert_eq!(map.get("host").unwrap(), "replaced");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retain_removes_empty_value_lists() {
|
||||||
|
let mut map = HeaderMap::with_capacity(3);
|
||||||
|
|
||||||
|
map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
||||||
|
map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
||||||
|
|
||||||
|
assert_eq!(map.len(), 2);
|
||||||
|
assert_eq!(map.len_keys(), 1);
|
||||||
|
assert_eq!(map.inner.len(), 1);
|
||||||
|
assert_eq!(map.capacity(), 3);
|
||||||
|
|
||||||
|
// remove everything
|
||||||
|
map.retain(|_n, _v| false);
|
||||||
|
|
||||||
|
assert_eq!(map.len(), 0);
|
||||||
|
assert_eq!(map.len_keys(), 0);
|
||||||
|
assert_eq!(map.inner.len(), 0);
|
||||||
|
assert_eq!(map.capacity(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn entries_into_iter() {
|
fn entries_into_iter() {
|
||||||
let mut map = HeaderMap::new();
|
let mut map = HeaderMap::new();
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
//! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods.
|
//! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods.
|
||||||
|
|
||||||
|
// declaring new header consts will yield this error
|
||||||
|
#![allow(clippy::declare_interior_mutable_const)]
|
||||||
|
|
||||||
use percent_encoding::{AsciiSet, CONTROLS};
|
use percent_encoding::{AsciiSet, CONTROLS};
|
||||||
|
|
||||||
// re-export from http except header map related items
|
// re-export from http except header map related items
|
||||||
pub use http::header::{
|
pub use ::http::header::{
|
||||||
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
|
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
|
||||||
};
|
};
|
||||||
|
|
||||||
// re-export const header names
|
// re-export const header names, list is explicit so that any updates to `common` module do not
|
||||||
pub use http::header::{
|
// conflict with this set
|
||||||
|
pub use ::http::header::{
|
||||||
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
|
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
|
||||||
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
|
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||||
|
@ -30,22 +34,30 @@ pub use http::header::{
|
||||||
use crate::{error::ParseError, HttpMessage};
|
use crate::{error::ParseError, HttpMessage};
|
||||||
|
|
||||||
mod as_name;
|
mod as_name;
|
||||||
|
mod common;
|
||||||
mod into_pair;
|
mod into_pair;
|
||||||
mod into_value;
|
mod into_value;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
mod shared;
|
mod shared;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use self::as_name::AsHeaderName;
|
pub use self::{
|
||||||
pub use self::into_pair::TryIntoHeaderPair;
|
as_name::AsHeaderName,
|
||||||
pub use self::into_value::TryIntoHeaderValue;
|
into_pair::TryIntoHeaderPair,
|
||||||
pub use self::map::HeaderMap;
|
into_value::TryIntoHeaderValue,
|
||||||
pub use self::shared::{
|
map::HeaderMap,
|
||||||
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag,
|
shared::{
|
||||||
Quality, QualityItem,
|
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate,
|
||||||
|
LanguageTag, Quality, QualityItem,
|
||||||
|
},
|
||||||
|
utils::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode},
|
||||||
};
|
};
|
||||||
pub use self::utils::{
|
|
||||||
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
|
// re-export list is explicit so that any updates to `http` do not conflict with this set
|
||||||
|
pub use self::common::{
|
||||||
|
CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY,
|
||||||
|
CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST,
|
||||||
|
X_FORWARDED_PROTO,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An interface for types that already represent a valid header.
|
/// An interface for types that already represent a valid header.
|
||||||
|
|
|
@ -21,7 +21,8 @@
|
||||||
#![allow(
|
#![allow(
|
||||||
clippy::type_complexity,
|
clippy::type_complexity,
|
||||||
clippy::too_many_arguments,
|
clippy::too_many_arguments,
|
||||||
clippy::borrow_interior_mutable_const
|
clippy::borrow_interior_mutable_const,
|
||||||
|
clippy::uninlined_format_args
|
||||||
)]
|
)]
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_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")]
|
||||||
|
|
|
@ -24,7 +24,39 @@ use crate::{
|
||||||
h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig,
|
h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
|
/// A [`ServiceFactory`] for HTTP/1.1 and HTTP/2 connections.
|
||||||
|
///
|
||||||
|
/// Use [`build`](Self::build) to begin constructing service. Also see [`HttpServiceBuilder`].
|
||||||
|
///
|
||||||
|
/// # Automatic HTTP Version Selection
|
||||||
|
/// There are two ways to select the HTTP version of an incoming connection:
|
||||||
|
/// - One is to rely on the ALPN information that is provided when using a TLS (HTTPS); both
|
||||||
|
/// versions are supported automatically when using either of the `.rustls()` or `.openssl()`
|
||||||
|
/// finalizing methods.
|
||||||
|
/// - The other is to read the first few bytes of the TCP stream. This is the only viable approach
|
||||||
|
/// for supporting H2C, which allows the HTTP/2 protocol to work over plaintext connections. Use
|
||||||
|
/// the `.tcp_auto_h2c()` finalizing method to enable this behavior.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use std::convert::Infallible;
|
||||||
|
/// use actix_http::{HttpService, Request, Response, StatusCode};
|
||||||
|
///
|
||||||
|
/// // this service would constructed in an actix_server::Server
|
||||||
|
///
|
||||||
|
/// # actix_rt::System::new().block_on(async {
|
||||||
|
/// HttpService::build()
|
||||||
|
/// // the builder finalizing method, other finalizers would not return an `HttpService`
|
||||||
|
/// .finish(|_req: Request| async move {
|
||||||
|
/// Ok::<_, Infallible>(
|
||||||
|
/// Response::build(StatusCode::OK).body("Hello!")
|
||||||
|
/// )
|
||||||
|
/// })
|
||||||
|
/// // the service finalizing method method
|
||||||
|
/// // you can use `.tcp_auto_h2c()`, `.rustls()`, or `.openssl()` instead of `.tcp()`
|
||||||
|
/// .tcp();
|
||||||
|
/// # })
|
||||||
|
/// ```
|
||||||
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
|
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
|
||||||
srv: S,
|
srv: S,
|
||||||
cfg: ServiceConfig,
|
cfg: ServiceConfig,
|
||||||
|
@ -163,7 +195,9 @@ where
|
||||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create simple tcp stream service
|
/// Creates TCP stream service from HTTP service.
|
||||||
|
///
|
||||||
|
/// The resulting service only supports HTTP/1.x.
|
||||||
pub fn tcp(
|
pub fn tcp(
|
||||||
self,
|
self,
|
||||||
) -> impl ServiceFactory<
|
) -> impl ServiceFactory<
|
||||||
|
@ -179,6 +213,42 @@ where
|
||||||
})
|
})
|
||||||
.and_then(self)
|
.and_then(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2
|
||||||
|
/// on plaintext connections.
|
||||||
|
#[cfg(feature = "http2")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||||
|
pub fn tcp_auto_h2c(
|
||||||
|
self,
|
||||||
|
) -> impl ServiceFactory<
|
||||||
|
TcpStream,
|
||||||
|
Config = (),
|
||||||
|
Response = (),
|
||||||
|
Error = DispatchError,
|
||||||
|
InitError = (),
|
||||||
|
> {
|
||||||
|
fn_service(move |io: TcpStream| async move {
|
||||||
|
// subset of HTTP/2 preface defined by RFC 9113 §3.4
|
||||||
|
// this subset was chosen to maximize likelihood that peeking only once will allow us to
|
||||||
|
// reliably determine version or else it should fallback to h1 and fail quickly if data
|
||||||
|
// on the wire is junk
|
||||||
|
const H2_PREFACE: &[u8] = b"PRI * HTTP/2";
|
||||||
|
|
||||||
|
let mut buf = [0; 12];
|
||||||
|
|
||||||
|
io.peek(&mut buf).await?;
|
||||||
|
|
||||||
|
let proto = if buf == H2_PREFACE {
|
||||||
|
Protocol::Http2
|
||||||
|
} else {
|
||||||
|
Protocol::Http1
|
||||||
|
};
|
||||||
|
|
||||||
|
let peer_addr = io.peer_addr().ok();
|
||||||
|
Ok((io, proto, peer_addr))
|
||||||
|
})
|
||||||
|
.and_then(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration options used when accepting TLS connection.
|
/// Configuration options used when accepting TLS connection.
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::{
|
||||||
fmt,
|
fmt,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use base64::prelude::*;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
/// Operation codes defined in [RFC 6455 §11.8].
|
/// Operation codes defined in [RFC 6455 §11.8].
|
||||||
|
@ -244,7 +245,7 @@ pub fn hash_key(key: &[u8]) -> [u8; 28] {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut hash_b64 = [0; 28];
|
let mut hash_b64 = [0; 28];
|
||||||
let n = base64::encode_config_slice(hash, base64::STANDARD, &mut hash_b64);
|
let n = BASE64_STANDARD.encode_slice(hash, &mut hash_b64).unwrap();
|
||||||
assert_eq!(n, 28);
|
assert_eq!(n, 28);
|
||||||
|
|
||||||
hash_b64
|
hash_b64
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#![cfg(feature = "openssl")]
|
#![cfg(feature = "openssl")]
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
extern crate tls_openssl as openssl;
|
extern crate tls_openssl as openssl;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#![cfg(feature = "rustls")]
|
#![cfg(feature = "rustls")]
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
extern crate tls_rustls as rustls;
|
extern crate tls_rustls as rustls;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
|
@ -7,10 +9,10 @@ use std::{
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
body::{self, BodyStream, BoxBody, SizedStream},
|
body::{self, BodyStream, BoxBody, SizedStream},
|
||||||
header, Error, HttpService, KeepAlive, Request, Response, StatusCode,
|
header, Error, HttpService, KeepAlive, Request, Response, StatusCode, Version,
|
||||||
};
|
};
|
||||||
use actix_http_test::test_server;
|
use actix_http_test::test_server;
|
||||||
use actix_rt::time::sleep;
|
use actix_rt::{net::TcpStream, time::sleep};
|
||||||
use actix_service::fn_service;
|
use actix_service::fn_service;
|
||||||
use actix_utils::future::{err, ok, ready};
|
use actix_utils::future::{err, ok, ready};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
@ -855,3 +857,44 @@ async fn not_modified_spec_h1() {
|
||||||
|
|
||||||
srv.stop().await;
|
srv.stop().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn h2c_auto() {
|
||||||
|
let mut srv = test_server(|| {
|
||||||
|
HttpService::build()
|
||||||
|
.keep_alive(KeepAlive::Disabled)
|
||||||
|
.finish(|req: Request| {
|
||||||
|
let body = match req.version() {
|
||||||
|
Version::HTTP_11 => "h1",
|
||||||
|
Version::HTTP_2 => "h2",
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
ok::<_, Infallible>(Response::ok().set_body(body))
|
||||||
|
})
|
||||||
|
.tcp_auto_h2c()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let req = srv.get("/");
|
||||||
|
assert_eq!(req.get_version(), &Version::HTTP_11);
|
||||||
|
let mut res = req.send().await.unwrap();
|
||||||
|
assert!(res.status().is_success());
|
||||||
|
assert_eq!(res.body().await.unwrap(), &b"h1"[..]);
|
||||||
|
|
||||||
|
// awc doesn't support forcing the version to http/2 so use h2 manually
|
||||||
|
|
||||||
|
let tcp = TcpStream::connect(srv.addr()).await.unwrap();
|
||||||
|
let (h2, connection) = h2::client::handshake(tcp).await.unwrap();
|
||||||
|
tokio::spawn(async move { connection.await.unwrap() });
|
||||||
|
let mut h2 = h2.ready().await.unwrap();
|
||||||
|
|
||||||
|
let request = ::http::Request::new(());
|
||||||
|
let (response, _) = h2.send_request(request, true).unwrap();
|
||||||
|
let (head, mut body) = response.await.unwrap().into_parts();
|
||||||
|
let body = body.data().await.unwrap().unwrap();
|
||||||
|
|
||||||
|
assert!(head.status.is_success());
|
||||||
|
assert_eq!(body, &b"h2"[..]);
|
||||||
|
|
||||||
|
srv.stop().await;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cell::Cell,
|
cell::Cell,
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
[package]
|
||||||
|
name = "actix-multipart-derive"
|
||||||
|
version = "0.5.0"
|
||||||
|
authors = ["Jacob Halsey <jacob@jhalsey.com>"]
|
||||||
|
description = "Multipart form derive macro for Actix Web"
|
||||||
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
|
homepage = "https://actix.rs"
|
||||||
|
repository = "https://github.com/actix/actix-web.git"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
darling = "0.14"
|
||||||
|
parse-size = "1"
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
syn = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
actix-multipart = "0.5"
|
||||||
|
actix-web = "4"
|
||||||
|
rustversion = "1"
|
||||||
|
trybuild = "1"
|
|
@ -0,0 +1 @@
|
||||||
|
../LICENSE-APACHE
|
|
@ -0,0 +1 @@
|
||||||
|
../LICENSE-MIT
|
|
@ -0,0 +1,3 @@
|
||||||
|
# actix-multipart-derive
|
||||||
|
|
||||||
|
> The derive macro implementation for actix-multipart.
|
|
@ -0,0 +1,315 @@
|
||||||
|
//! Multipart form derive macro for Actix Web.
|
||||||
|
//!
|
||||||
|
//! See [`macro@MultipartForm`] for usage examples.
|
||||||
|
|
||||||
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
|
#![warn(future_incompatible)]
|
||||||
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
use std::{collections::HashSet, convert::TryFrom as _};
|
||||||
|
|
||||||
|
use darling::{FromDeriveInput, FromField, FromMeta};
|
||||||
|
use parse_size::parse_size;
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::Ident;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse_macro_input, Type};
|
||||||
|
|
||||||
|
#[derive(FromMeta)]
|
||||||
|
enum DuplicateField {
|
||||||
|
Ignore,
|
||||||
|
Deny,
|
||||||
|
Replace,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DuplicateField {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromDeriveInput, Default)]
|
||||||
|
#[darling(attributes(multipart), default)]
|
||||||
|
struct MultipartFormAttrs {
|
||||||
|
deny_unknown_fields: bool,
|
||||||
|
duplicate_field: DuplicateField,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromField, Default)]
|
||||||
|
#[darling(attributes(multipart), default)]
|
||||||
|
struct FieldAttrs {
|
||||||
|
rename: Option<String>,
|
||||||
|
limit: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParsedField<'t> {
|
||||||
|
serialization_name: String,
|
||||||
|
rust_name: &'t Ident,
|
||||||
|
limit: Option<usize>,
|
||||||
|
ty: &'t Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements `MultipartCollect` for a struct so that it can be used with the `MultipartForm`
|
||||||
|
/// extractor.
|
||||||
|
///
|
||||||
|
/// # Basic Use
|
||||||
|
///
|
||||||
|
/// Each field type should implement the `FieldReader` trait:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
||||||
|
///
|
||||||
|
/// #[derive(MultipartForm)]
|
||||||
|
/// struct ImageUpload {
|
||||||
|
/// description: Text<String>,
|
||||||
|
/// timestamp: Text<i64>,
|
||||||
|
/// image: TempFile,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Optional and List Fields
|
||||||
|
///
|
||||||
|
/// You can also use `Vec<T>` and `Option<T>` provided that `T: FieldReader`.
|
||||||
|
///
|
||||||
|
/// A [`Vec`] field corresponds to an upload with multiple parts under the [same field
|
||||||
|
/// name](https://www.rfc-editor.org/rfc/rfc7578#section-4.3).
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
||||||
|
///
|
||||||
|
/// #[derive(MultipartForm)]
|
||||||
|
/// struct Form {
|
||||||
|
/// category: Option<Text<String>>,
|
||||||
|
/// files: Vec<TempFile>,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Field Renaming
|
||||||
|
///
|
||||||
|
/// You can use the `#[multipart(rename = "foo")]` attribute to receive a field by a different name.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix_multipart::form::{tempfile::TempFile, MultipartForm};
|
||||||
|
///
|
||||||
|
/// #[derive(MultipartForm)]
|
||||||
|
/// struct Form {
|
||||||
|
/// #[multipart(rename = "files[]")]
|
||||||
|
/// files: Vec<TempFile>,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Field Limits
|
||||||
|
///
|
||||||
|
/// You can use the `#[multipart(limit = "<size>")]` attribute to set field level limits. The limit
|
||||||
|
/// string is parsed using [parse_size].
|
||||||
|
///
|
||||||
|
/// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
||||||
|
///
|
||||||
|
/// #[derive(MultipartForm)]
|
||||||
|
/// struct Form {
|
||||||
|
/// #[multipart(limit = "2 KiB")]
|
||||||
|
/// description: Text<String>,
|
||||||
|
///
|
||||||
|
/// #[multipart(limit = "512 MiB")]
|
||||||
|
/// files: Vec<TempFile>,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Unknown Fields
|
||||||
|
///
|
||||||
|
/// By default fields with an unknown name are ignored. They can be rejected using the
|
||||||
|
/// `#[multipart(deny_unknown_fields)]` attribute:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_multipart::form::MultipartForm;
|
||||||
|
/// #[derive(MultipartForm)]
|
||||||
|
/// #[multipart(deny_unknown_fields)]
|
||||||
|
/// struct Form { }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Duplicate Fields
|
||||||
|
///
|
||||||
|
/// The behaviour for when multiple fields with the same name are received can be changed using the
|
||||||
|
/// `#[multipart(duplicate_field = "<behavior>")]` attribute:
|
||||||
|
///
|
||||||
|
/// - "ignore": (default) Extra fields are ignored. I.e., the first one is persisted.
|
||||||
|
/// - "deny": A `MultipartError::UnsupportedField` error response is returned.
|
||||||
|
/// - "replace": Each field is processed, but only the last one is persisted.
|
||||||
|
///
|
||||||
|
/// Note that `Vec` fields will ignore this option.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_multipart::form::MultipartForm;
|
||||||
|
/// #[derive(MultipartForm)]
|
||||||
|
/// #[multipart(duplicate_field = "deny")]
|
||||||
|
/// struct Form { }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [parse_size]: https://docs.rs/parse-size/1/parse_size
|
||||||
|
#[proc_macro_derive(MultipartForm, attributes(multipart))]
|
||||||
|
pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
let input: syn::DeriveInput = parse_macro_input!(input);
|
||||||
|
|
||||||
|
let name = &input.ident;
|
||||||
|
|
||||||
|
let data_struct = match &input.data {
|
||||||
|
syn::Data::Struct(data_struct) => data_struct,
|
||||||
|
_ => {
|
||||||
|
return compile_err(syn::Error::new(
|
||||||
|
input.ident.span(),
|
||||||
|
"`MultipartForm` can only be derived for structs",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let fields = match &data_struct.fields {
|
||||||
|
syn::Fields::Named(fields_named) => fields_named,
|
||||||
|
_ => {
|
||||||
|
return compile_err(syn::Error::new(
|
||||||
|
input.ident.span(),
|
||||||
|
"`MultipartForm` can only be derived for a struct with named fields",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let attrs = match MultipartFormAttrs::from_derive_input(&input) {
|
||||||
|
Ok(attrs) => attrs,
|
||||||
|
Err(err) => return err.write_errors().into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the field attributes
|
||||||
|
let parsed = match fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|field| {
|
||||||
|
let rust_name = field.ident.as_ref().unwrap();
|
||||||
|
let attrs = FieldAttrs::from_field(field).map_err(|err| err.write_errors())?;
|
||||||
|
let serialization_name = attrs.rename.unwrap_or_else(|| rust_name.to_string());
|
||||||
|
|
||||||
|
let limit = match attrs.limit.map(|limit| match parse_size(&limit) {
|
||||||
|
Ok(size) => Ok(usize::try_from(size).unwrap()),
|
||||||
|
Err(err) => Err(syn::Error::new(
|
||||||
|
field.ident.as_ref().unwrap().span(),
|
||||||
|
format!("Could not parse size limit `{}`: {}", limit, err),
|
||||||
|
)),
|
||||||
|
}) {
|
||||||
|
Some(Err(err)) => return Err(compile_err(err)),
|
||||||
|
limit => limit.map(Result::unwrap),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ParsedField {
|
||||||
|
serialization_name,
|
||||||
|
rust_name,
|
||||||
|
limit,
|
||||||
|
ty: &field.ty,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, TokenStream>>()
|
||||||
|
{
|
||||||
|
Ok(attrs) => attrs,
|
||||||
|
Err(err) => return err,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check that field names are unique
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
for field in &parsed {
|
||||||
|
if !set.insert(field.serialization_name.clone()) {
|
||||||
|
return compile_err(syn::Error::new(
|
||||||
|
field.rust_name.span(),
|
||||||
|
format!("Multiple fields named: `{}`", field.serialization_name),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return value when a field name is not supported by the form
|
||||||
|
let unknown_field_result = if attrs.deny_unknown_fields {
|
||||||
|
quote!(::std::result::Result::Err(
|
||||||
|
::actix_multipart::MultipartError::UnsupportedField(field.name().to_string())
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
quote!(::std::result::Result::Ok(()))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Value for duplicate action
|
||||||
|
let duplicate_field = match attrs.duplicate_field {
|
||||||
|
DuplicateField::Ignore => quote!(::actix_multipart::form::DuplicateField::Ignore),
|
||||||
|
DuplicateField::Deny => quote!(::actix_multipart::form::DuplicateField::Deny),
|
||||||
|
DuplicateField::Replace => quote!(::actix_multipart::form::DuplicateField::Replace),
|
||||||
|
};
|
||||||
|
|
||||||
|
// limit() implementation
|
||||||
|
let mut limit_impl = quote!();
|
||||||
|
for field in &parsed {
|
||||||
|
let name = &field.serialization_name;
|
||||||
|
if let Some(value) = field.limit {
|
||||||
|
limit_impl.extend(quote!(
|
||||||
|
#name => ::std::option::Option::Some(#value),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle_field() implementation
|
||||||
|
let mut handle_field_impl = quote!();
|
||||||
|
for field in &parsed {
|
||||||
|
let name = &field.serialization_name;
|
||||||
|
let ty = &field.ty;
|
||||||
|
|
||||||
|
handle_field_impl.extend(quote!(
|
||||||
|
#name => ::std::boxed::Box::pin(
|
||||||
|
<#ty as ::actix_multipart::form::FieldGroupReader>::handle_field(req, field, limits, state, #duplicate_field)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// from_state() implementation
|
||||||
|
let mut from_state_impl = quote!();
|
||||||
|
for field in &parsed {
|
||||||
|
let name = &field.serialization_name;
|
||||||
|
let rust_name = &field.rust_name;
|
||||||
|
let ty = &field.ty;
|
||||||
|
from_state_impl.extend(quote!(
|
||||||
|
#rust_name: <#ty as ::actix_multipart::form::FieldGroupReader>::from_state(#name, &mut state)?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let gen = quote! {
|
||||||
|
impl ::actix_multipart::form::MultipartCollect for #name {
|
||||||
|
fn limit(field_name: &str) -> ::std::option::Option<usize> {
|
||||||
|
match field_name {
|
||||||
|
#limit_impl
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_field<'t>(
|
||||||
|
req: &'t ::actix_web::HttpRequest,
|
||||||
|
field: ::actix_multipart::Field,
|
||||||
|
limits: &'t mut ::actix_multipart::form::Limits,
|
||||||
|
state: &'t mut ::actix_multipart::form::State,
|
||||||
|
) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::actix_multipart::MultipartError>> + 't>> {
|
||||||
|
match field.name() {
|
||||||
|
#handle_field_impl
|
||||||
|
_ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_state(mut state: ::actix_multipart::form::State) -> ::std::result::Result<Self, ::actix_multipart::MultipartError> {
|
||||||
|
Ok(Self {
|
||||||
|
#from_state_impl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
gen.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform a syn error into a token stream for returning.
|
||||||
|
fn compile_err(err: syn::Error) -> TokenStream {
|
||||||
|
TokenStream::from(err.to_compile_error())
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
#[rustversion::stable(1.59)] // MSRV
|
||||||
|
#[test]
|
||||||
|
fn compile_macros() {
|
||||||
|
let t = trybuild::TestCases::new();
|
||||||
|
|
||||||
|
t.pass("tests/trybuild/all-required.rs");
|
||||||
|
t.pass("tests/trybuild/optional-and-list.rs");
|
||||||
|
t.pass("tests/trybuild/rename.rs");
|
||||||
|
t.pass("tests/trybuild/deny-unknown.rs");
|
||||||
|
|
||||||
|
t.pass("tests/trybuild/deny-duplicates.rs");
|
||||||
|
t.compile_fail("tests/trybuild/deny-parse-fail.rs");
|
||||||
|
|
||||||
|
t.pass("tests/trybuild/size-limits.rs");
|
||||||
|
t.compile_fail("tests/trybuild/size-limit-parse-fail.rs");
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
use actix_web::{web, App, Responder};
|
||||||
|
|
||||||
|
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
||||||
|
|
||||||
|
#[derive(Debug, MultipartForm)]
|
||||||
|
struct ImageUpload {
|
||||||
|
description: Text<String>,
|
||||||
|
timestamp: Text<i64>,
|
||||||
|
image: TempFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler(_form: MultipartForm<ImageUpload>) -> impl Responder {
|
||||||
|
"Hello World!"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
App::new().default_service(web::to(handler));
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
use actix_web::{web, App, Responder};
|
||||||
|
|
||||||
|
use actix_multipart::form::MultipartForm;
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
#[multipart(duplicate_field = "deny")]
|
||||||
|
struct Form {}
|
||||||
|
|
||||||
|
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
|
||||||
|
"Hello World!"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
App::new().default_service(web::to(handler));
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
use actix_multipart::form::MultipartForm;
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
#[multipart(duplicate_field = "no")]
|
||||||
|
struct Form {}
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -0,0 +1,5 @@
|
||||||
|
error: Unknown literal value `no`
|
||||||
|
--> tests/trybuild/deny-parse-fail.rs:4:31
|
||||||
|
|
|
||||||
|
4 | #[multipart(duplicate_field = "no")]
|
||||||
|
| ^^^^
|
|
@ -0,0 +1,16 @@
|
||||||
|
use actix_web::{web, App, Responder};
|
||||||
|
|
||||||
|
use actix_multipart::form::MultipartForm;
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
#[multipart(deny_unknown_fields)]
|
||||||
|
struct Form {}
|
||||||
|
|
||||||
|
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
|
||||||
|
"Hello World!"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
App::new().default_service(web::to(handler));
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
use actix_web::{web, App, Responder};
|
||||||
|
|
||||||
|
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct Form {
|
||||||
|
category: Option<Text<String>>,
|
||||||
|
files: Vec<TempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
|
||||||
|
"Hello World!"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
App::new().default_service(web::to(handler));
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
use actix_web::{web, App, Responder};
|
||||||
|
|
||||||
|
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct Form {
|
||||||
|
#[multipart(rename = "files[]")]
|
||||||
|
files: Vec<TempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
|
||||||
|
"Hello World!"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
App::new().default_service(web::to(handler));
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
use actix_multipart::form::{text::Text, MultipartForm};
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct Form {
|
||||||
|
#[multipart(limit = "2 bytes")]
|
||||||
|
description: Text<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct Form2 {
|
||||||
|
#[multipart(limit = "2 megabytes")]
|
||||||
|
description: Text<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct Form3 {
|
||||||
|
#[multipart(limit = "four meters")]
|
||||||
|
description: Text<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -0,0 +1,17 @@
|
||||||
|
error: Could not parse size limit `2 bytes`: invalid digit found in string
|
||||||
|
--> tests/trybuild/size-limit-parse-fail.rs:6:5
|
||||||
|
|
|
||||||
|
6 | description: Text<String>,
|
||||||
|
| ^^^^^^^^^^^
|
||||||
|
|
||||||
|
error: Could not parse size limit `2 megabytes`: invalid digit found in string
|
||||||
|
--> tests/trybuild/size-limit-parse-fail.rs:12:5
|
||||||
|
|
|
||||||
|
12 | description: Text<String>,
|
||||||
|
| ^^^^^^^^^^^
|
||||||
|
|
||||||
|
error: Could not parse size limit `four meters`: invalid digit found in string
|
||||||
|
--> tests/trybuild/size-limit-parse-fail.rs:18:5
|
||||||
|
|
|
||||||
|
18 | description: Text<String>,
|
||||||
|
| ^^^^^^^^^^^
|
|
@ -0,0 +1,21 @@
|
||||||
|
use actix_web::{web, App, Responder};
|
||||||
|
|
||||||
|
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct Form {
|
||||||
|
#[multipart(limit = "2 KiB")]
|
||||||
|
description: Text<String>,
|
||||||
|
|
||||||
|
#[multipart(limit = "512 MiB")]
|
||||||
|
files: Vec<TempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler(_form: MultipartForm<Form>) -> impl Responder {
|
||||||
|
"Hello World!"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
App::new().default_service(web::to(handler));
|
||||||
|
}
|
|
@ -1,39 +1,46 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
- Added `MultipartForm` typed data extractor. [#2883]
|
||||||
|
|
||||||
|
[#2883]: https://github.com/actix/actix-web/pull/2883
|
||||||
|
|
||||||
|
## 0.5.0 - 2023-01-21
|
||||||
|
|
||||||
|
- `Field::content_type()` now returns `Option<&mime::Mime>`. [#2885]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||||
- `Field::content_type()` now returns `Option<&mime::Mime>` [#2880]
|
|
||||||
|
|
||||||
[#2880]: https://github.com/actix/actix-web/pull/2880
|
|
||||||
|
|
||||||
|
[#2885]: https://github.com/actix/actix-web/pull/2885
|
||||||
|
|
||||||
## 0.4.0 - 2022-02-25
|
## 0.4.0 - 2022-02-25
|
||||||
|
|
||||||
- No significant changes since `0.4.0-beta.13`.
|
- No significant changes since `0.4.0-beta.13`.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.13 - 2022-01-31
|
## 0.4.0-beta.13 - 2022-01-31
|
||||||
|
|
||||||
- No significant changes since `0.4.0-beta.12`.
|
- No significant changes since `0.4.0-beta.12`.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.12 - 2022-01-04
|
## 0.4.0-beta.12 - 2022-01-04
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.11 - 2021-12-27
|
## 0.4.0-beta.11 - 2021-12-27
|
||||||
|
|
||||||
- No significant changes since `0.4.0-beta.10`.
|
- No significant changes since `0.4.0-beta.10`.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.10 - 2021-12-11
|
## 0.4.0-beta.10 - 2021-12-11
|
||||||
|
|
||||||
- No significant changes since `0.4.0-beta.9`.
|
- No significant changes since `0.4.0-beta.9`.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.9 - 2021-12-01
|
## 0.4.0-beta.9 - 2021-12-01
|
||||||
|
|
||||||
- Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463]
|
- Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463]
|
||||||
|
|
||||||
[#2463]: https://github.com/actix/actix-web/pull/2463
|
[#2463]: https://github.com/actix/actix-web/pull/2463
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.8 - 2021-11-22
|
## 0.4.0-beta.8 - 2021-11-22
|
||||||
|
|
||||||
- Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451]
|
- Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451]
|
||||||
- Added `MultipartError::NoContentDisposition` variant. [#2451]
|
- Added `MultipartError::NoContentDisposition` variant. [#2451]
|
||||||
- Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451]
|
- Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451]
|
||||||
|
@ -43,52 +50,52 @@
|
||||||
|
|
||||||
[#2451]: https://github.com/actix/actix-web/pull/2451
|
[#2451]: https://github.com/actix/actix-web/pull/2451
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.7 - 2021-10-20
|
## 0.4.0-beta.7 - 2021-10-20
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.6 - 2021-09-09
|
## 0.4.0-beta.6 - 2021-09-09
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.5 - 2021-06-17
|
## 0.4.0-beta.5 - 2021-06-17
|
||||||
- No notable changes.
|
|
||||||
|
|
||||||
|
- No notable changes.
|
||||||
|
|
||||||
## 0.4.0-beta.4 - 2021-04-02
|
## 0.4.0-beta.4 - 2021-04-02
|
||||||
- No notable changes.
|
|
||||||
|
|
||||||
|
- No notable changes.
|
||||||
|
|
||||||
## 0.4.0-beta.3 - 2021-03-09
|
## 0.4.0-beta.3 - 2021-03-09
|
||||||
- No notable changes.
|
|
||||||
|
|
||||||
|
- No notable changes.
|
||||||
|
|
||||||
## 0.4.0-beta.2 - 2021-02-10
|
## 0.4.0-beta.2 - 2021-02-10
|
||||||
|
|
||||||
- No notable changes.
|
- No notable changes.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.1 - 2021-01-07
|
## 0.4.0-beta.1 - 2021-01-07
|
||||||
|
|
||||||
- Fix multipart consuming payload before header checks. [#1513]
|
- Fix multipart consuming payload before header checks. [#1513]
|
||||||
- Update `bytes` to `1.0`. [#1813]
|
- Update `bytes` to `1.0`. [#1813]
|
||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
[#1513]: https://github.com/actix/actix-web/pull/1513
|
[#1513]: https://github.com/actix/actix-web/pull/1513
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0 - 2020-09-11
|
## 0.3.0 - 2020-09-11
|
||||||
|
|
||||||
- No significant changes from `0.3.0-beta.2`.
|
- No significant changes from `0.3.0-beta.2`.
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0-beta.2 - 2020-09-10
|
## 0.3.0-beta.2 - 2020-09-10
|
||||||
|
|
||||||
- Update `actix-*` dependencies to latest versions.
|
- Update `actix-*` dependencies to latest versions.
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0-beta.1 - 2020-07-15
|
## 0.3.0-beta.1 - 2020-07-15
|
||||||
|
|
||||||
- Update `actix-web` to 3.0.0-beta.1
|
- Update `actix-web` to 3.0.0-beta.1
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0-alpha.1 - 2020-05-25
|
## 0.3.0-alpha.1 - 2020-05-25
|
||||||
|
|
||||||
- Update `actix-web` to 3.0.0-alpha.3
|
- Update `actix-web` to 3.0.0-alpha.3
|
||||||
- Bump minimum supported Rust version to 1.40
|
- Bump minimum supported Rust version to 1.40
|
||||||
- Minimize `futures` dependencies
|
- Minimize `futures` dependencies
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-multipart"
|
name = "actix-multipart"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"Jacob Halsey <jacob@jhalsey.com>",
|
||||||
|
]
|
||||||
description = "Multipart form support for Actix Web"
|
description = "Multipart form support for Actix Web"
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
|
@ -9,26 +12,46 @@ repository = "https://github.com/actix/actix-web.git"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
all-features = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["tempfile", "derive"]
|
||||||
|
derive = ["actix-multipart-derive"]
|
||||||
|
tempfile = ["tempfile-dep", "tokio/fs"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_multipart"
|
name = "actix_multipart"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
actix-multipart-derive = { version = "=0.5.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"
|
bytes = "1"
|
||||||
derive_more = "0.99.5"
|
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"] }
|
||||||
httparse = "1.3"
|
httparse = "1.3"
|
||||||
local-waker = "0.1"
|
local-waker = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
|
||||||
memchr = "2.5"
|
memchr = "2.5"
|
||||||
|
mime = "0.3"
|
||||||
|
serde = "1"
|
||||||
|
serde_json = "1"
|
||||||
|
serde_plain = "1"
|
||||||
|
# TODO(MSRV 1.60): replace with dep: prefix
|
||||||
|
tempfile-dep = { package = "tempfile", version = "3.4", optional = true }
|
||||||
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
|
||||||
actix-http = "3"
|
actix-http = "3"
|
||||||
|
actix-multipart-rfc7578 = "0.10"
|
||||||
|
actix-rt = "2.2"
|
||||||
|
actix-test = "0.1"
|
||||||
|
awc = "3"
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||||
tokio = { version = "1.8.4", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
> Multipart form support for Actix Web.
|
> Multipart form support for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-multipart)
|
[](https://crates.io/crates/actix-multipart)
|
||||||
[](https://docs.rs/actix-multipart/0.4.0)
|
[](https://docs.rs/actix-multipart/0.5.0)
|
||||||

|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-multipart/0.4.0)
|
[](https://deps.rs/crate/actix-multipart/0.5.0)
|
||||||
[](https://crates.io/crates/actix-multipart)
|
[](https://crates.io/crates/actix-multipart)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-multipart)
|
- [API Documentation](https://docs.rs/actix-multipart)
|
||||||
- Minimum Supported Rust Version (MSRV): 1.54
|
- Minimum Supported Rust Version (MSRV): 1.59
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
//! Error and Result module
|
//! Error and Result module
|
||||||
use actix_web::error::{ParseError, PayloadError};
|
|
||||||
use actix_web::http::StatusCode;
|
use actix_web::{
|
||||||
use actix_web::ResponseError;
|
error::{ParseError, PayloadError},
|
||||||
|
http::StatusCode,
|
||||||
|
ResponseError,
|
||||||
|
};
|
||||||
use derive_more::{Display, Error, From};
|
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.
|
||||||
#[non_exhaustive]
|
|
||||||
#[derive(Debug, Display, From, Error)]
|
#[derive(Debug, Display, From, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum MultipartError {
|
pub enum MultipartError {
|
||||||
/// Content-Disposition header is not found or is not equal to "form-data".
|
/// Content-Disposition header is not found or is not equal to "form-data".
|
||||||
///
|
///
|
||||||
|
@ -46,12 +49,41 @@ pub enum MultipartError {
|
||||||
/// Not consumed
|
/// Not consumed
|
||||||
#[display(fmt = "Multipart stream is not consumed")]
|
#[display(fmt = "Multipart stream is not consumed")]
|
||||||
NotConsumed,
|
NotConsumed,
|
||||||
|
|
||||||
|
/// An error from a field handler in a form
|
||||||
|
#[display(
|
||||||
|
fmt = "An error occurred processing field `{}`: {}",
|
||||||
|
field_name,
|
||||||
|
source
|
||||||
|
)]
|
||||||
|
Field {
|
||||||
|
field_name: String,
|
||||||
|
source: actix_web::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Duplicate field
|
||||||
|
#[display(fmt = "Duplicate field found for: `{}`", _0)]
|
||||||
|
#[from(ignore)]
|
||||||
|
DuplicateField(#[error(not(source))] String),
|
||||||
|
|
||||||
|
/// Missing field
|
||||||
|
#[display(fmt = "Field with name `{}` is required", _0)]
|
||||||
|
#[from(ignore)]
|
||||||
|
MissingField(#[error(not(source))] String),
|
||||||
|
|
||||||
|
/// Unknown field
|
||||||
|
#[display(fmt = "Unsupported field `{}`", _0)]
|
||||||
|
#[from(ignore)]
|
||||||
|
UnsupportedField(#[error(not(source))] String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `BadRequest` for `MultipartError`
|
/// Return `BadRequest` for `MultipartError`
|
||||||
impl ResponseError for MultipartError {
|
impl ResponseError for MultipartError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
StatusCode::BAD_REQUEST
|
match &self {
|
||||||
|
MultipartError::Field { source, .. } => source.as_response_error().status_code(),
|
||||||
|
_ => StatusCode::BAD_REQUEST,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,7 @@ use crate::server::Multipart;
|
||||||
///
|
///
|
||||||
/// Content-type: multipart/form-data;
|
/// Content-type: multipart/form-data;
|
||||||
///
|
///
|
||||||
/// ## Server example
|
/// # Examples
|
||||||
///
|
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, HttpResponse, Error};
|
/// use actix_web::{web, HttpResponse, Error};
|
||||||
/// use actix_multipart::Multipart;
|
/// use actix_multipart::Multipart;
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
//! Reads a field into memory.
|
||||||
|
|
||||||
|
use actix_web::HttpRequest;
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
use futures_util::TryStreamExt as _;
|
||||||
|
use mime::Mime;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
form::{FieldReader, Limits},
|
||||||
|
Field, MultipartError,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read the field into memory.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Bytes {
|
||||||
|
/// The data.
|
||||||
|
pub data: bytes::Bytes,
|
||||||
|
|
||||||
|
/// The value of the `Content-Type` header.
|
||||||
|
pub content_type: Option<Mime>,
|
||||||
|
|
||||||
|
/// The `filename` value in the `Content-Disposition` header.
|
||||||
|
pub file_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> FieldReader<'t> for Bytes {
|
||||||
|
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
|
||||||
|
|
||||||
|
fn read_field(
|
||||||
|
_: &'t HttpRequest,
|
||||||
|
mut field: Field,
|
||||||
|
limits: &'t mut Limits,
|
||||||
|
) -> Self::Future {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut buf = BytesMut::with_capacity(131_072);
|
||||||
|
|
||||||
|
while let Some(chunk) = field.try_next().await? {
|
||||||
|
limits.try_consume_limits(chunk.len(), true)?;
|
||||||
|
buf.extend(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Bytes {
|
||||||
|
data: buf.freeze(),
|
||||||
|
content_type: field.content_type().map(ToOwned::to_owned),
|
||||||
|
file_name: field
|
||||||
|
.content_disposition()
|
||||||
|
.get_filename()
|
||||||
|
.map(str::to_owned),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
//! Deserializes a field as JSON.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError};
|
||||||
|
use derive_more::{Deref, DerefMut, Display, Error};
|
||||||
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
form::{bytes::Bytes, FieldReader, Limits},
|
||||||
|
Field, MultipartError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::FieldErrorHandler;
|
||||||
|
|
||||||
|
/// Deserialize from JSON.
|
||||||
|
#[derive(Debug, Deref, DerefMut)]
|
||||||
|
pub struct Json<T: DeserializeOwned>(pub T);
|
||||||
|
|
||||||
|
impl<T: DeserializeOwned> Json<T> {
|
||||||
|
pub fn into_inner(self) -> T {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t, T> FieldReader<'t> for Json<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + 'static,
|
||||||
|
{
|
||||||
|
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
|
||||||
|
|
||||||
|
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future {
|
||||||
|
Box::pin(async move {
|
||||||
|
let config = JsonConfig::from_req(req);
|
||||||
|
let field_name = field.name().to_owned();
|
||||||
|
|
||||||
|
if config.validate_content_type {
|
||||||
|
let valid = if let Some(mime) = field.content_type() {
|
||||||
|
mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return Err(MultipartError::Field {
|
||||||
|
field_name,
|
||||||
|
source: config.map_error(req, JsonFieldError::ContentType),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = Bytes::read_field(req, field, limits).await?;
|
||||||
|
|
||||||
|
Ok(Json(serde_json::from_slice(bytes.data.as_ref()).map_err(
|
||||||
|
|err| MultipartError::Field {
|
||||||
|
field_name,
|
||||||
|
source: config.map_error(req, JsonFieldError::Deserialize(err)),
|
||||||
|
},
|
||||||
|
)?))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum JsonFieldError {
|
||||||
|
/// Deserialize error.
|
||||||
|
#[display(fmt = "Json deserialize error: {}", _0)]
|
||||||
|
Deserialize(serde_json::Error),
|
||||||
|
|
||||||
|
/// Content type error.
|
||||||
|
#[display(fmt = "Content type error")]
|
||||||
|
ContentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for JsonFieldError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the [`Json`] field reader.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct JsonConfig {
|
||||||
|
err_handler: FieldErrorHandler<JsonFieldError>,
|
||||||
|
validate_content_type: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: JsonConfig = JsonConfig {
|
||||||
|
err_handler: None,
|
||||||
|
validate_content_type: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl JsonConfig {
|
||||||
|
pub fn error_handler<F>(mut self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(JsonFieldError, &HttpRequest) -> Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.err_handler = Some(Arc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
|
||||||
|
/// back to the default payload config.
|
||||||
|
fn from_req(req: &HttpRequest) -> &Self {
|
||||||
|
req.app_data::<Self>()
|
||||||
|
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
|
||||||
|
.unwrap_or(&DEFAULT_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_error(&self, req: &HttpRequest, err: JsonFieldError) -> Error {
|
||||||
|
if let Some(err_handler) = self.err_handler.as_ref() {
|
||||||
|
(*err_handler)(err, req)
|
||||||
|
} else {
|
||||||
|
err.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether or not the field must have a valid `Content-Type` header to be parsed.
|
||||||
|
pub fn validate_content_type(mut self, validate_content_type: bool) -> Self {
|
||||||
|
self.validate_content_type = validate_content_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for JsonConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
DEFAULT_CONFIG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{collections::HashMap, io::Cursor};
|
||||||
|
|
||||||
|
use actix_multipart_rfc7578::client::multipart;
|
||||||
|
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
|
||||||
|
|
||||||
|
use crate::form::{
|
||||||
|
json::{Json, JsonConfig},
|
||||||
|
tests::send_form,
|
||||||
|
MultipartForm,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct JsonForm {
|
||||||
|
json: Json<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_json_route(form: MultipartForm<JsonForm>) -> impl Responder {
|
||||||
|
let mut expected = HashMap::new();
|
||||||
|
expected.insert("key1".to_owned(), "value1".to_owned());
|
||||||
|
expected.insert("key2".to_owned(), "value2".to_owned());
|
||||||
|
assert_eq!(&*form.json, &expected);
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_json_without_content_type() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.route("/", web::post().to(test_json_route))
|
||||||
|
.app_data(JsonConfig::default().validate_content_type(false))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("json", "{\"key1\": \"value1\", \"key2\": \"value2\"}");
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_content_type_validation() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.route("/", web::post().to(test_json_route))
|
||||||
|
.app_data(JsonConfig::default().validate_content_type(true))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deny because wrong content type
|
||||||
|
let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_OCTET_STREAM);
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
// Allow because correct content type
|
||||||
|
let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_JSON);
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,744 @@
|
||||||
|
//! Process and extract typed data from a multipart stream.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
any::Any,
|
||||||
|
collections::HashMap,
|
||||||
|
future::{ready, Future},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_web::{dev, error::PayloadError, web, Error, FromRequest, HttpRequest};
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
use futures_util::{TryFutureExt as _, TryStreamExt as _};
|
||||||
|
|
||||||
|
use crate::{Field, Multipart, MultipartError};
|
||||||
|
|
||||||
|
pub mod bytes;
|
||||||
|
pub mod json;
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "tempfile")))]
|
||||||
|
#[cfg(feature = "tempfile")]
|
||||||
|
pub mod tempfile;
|
||||||
|
pub mod text;
|
||||||
|
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
|
||||||
|
#[cfg(feature = "derive")]
|
||||||
|
pub use actix_multipart_derive::MultipartForm;
|
||||||
|
|
||||||
|
type FieldErrorHandler<T> = Option<Arc<dyn Fn(T, &HttpRequest) -> Error + Send + Sync>>;
|
||||||
|
|
||||||
|
/// Trait that data types to be used in a multipart form struct should implement.
|
||||||
|
///
|
||||||
|
/// It represents an asynchronous handler that processes a multipart field to produce `Self`.
|
||||||
|
pub trait FieldReader<'t>: Sized + Any {
|
||||||
|
/// Future that resolves to a `Self`.
|
||||||
|
type Future: Future<Output = Result<Self, MultipartError>>;
|
||||||
|
|
||||||
|
/// The form will call this function to handle the field.
|
||||||
|
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to accumulate the state of the loaded fields.
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[derive(Default, Deref, DerefMut)]
|
||||||
|
pub struct State(pub HashMap<String, Box<dyn Any>>);
|
||||||
|
|
||||||
|
/// Trait that the field collection types implement, i.e. `Vec<T>`, `Option<T>`, or `T` itself.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub trait FieldGroupReader<'t>: Sized + Any {
|
||||||
|
type Future: Future<Output = Result<(), MultipartError>>;
|
||||||
|
|
||||||
|
/// The form will call this function for each matching field.
|
||||||
|
fn handle_field(
|
||||||
|
req: &'t HttpRequest,
|
||||||
|
field: Field,
|
||||||
|
limits: &'t mut Limits,
|
||||||
|
state: &'t mut State,
|
||||||
|
duplicate_field: DuplicateField,
|
||||||
|
) -> Self::Future;
|
||||||
|
|
||||||
|
/// Construct `Self` from the group of processed fields.
|
||||||
|
fn from_state(name: &str, state: &'t mut State) -> Result<Self, MultipartError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t, T> FieldGroupReader<'t> for Option<T>
|
||||||
|
where
|
||||||
|
T: FieldReader<'t>,
|
||||||
|
{
|
||||||
|
type Future = LocalBoxFuture<'t, Result<(), MultipartError>>;
|
||||||
|
|
||||||
|
fn handle_field(
|
||||||
|
req: &'t HttpRequest,
|
||||||
|
field: Field,
|
||||||
|
limits: &'t mut Limits,
|
||||||
|
state: &'t mut State,
|
||||||
|
duplicate_field: DuplicateField,
|
||||||
|
) -> Self::Future {
|
||||||
|
if state.contains_key(field.name()) {
|
||||||
|
match duplicate_field {
|
||||||
|
DuplicateField::Ignore => return Box::pin(ready(Ok(()))),
|
||||||
|
|
||||||
|
DuplicateField::Deny => {
|
||||||
|
return Box::pin(ready(Err(MultipartError::DuplicateField(
|
||||||
|
field.name().to_owned(),
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
DuplicateField::Replace => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let field_name = field.name().to_owned();
|
||||||
|
let t = T::read_field(req, field, limits).await?;
|
||||||
|
state.insert(field_name, Box::new(t));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_state(name: &str, state: &'t mut State) -> Result<Self, MultipartError> {
|
||||||
|
Ok(state.remove(name).map(|m| *m.downcast::<T>().unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t, T> FieldGroupReader<'t> for Vec<T>
|
||||||
|
where
|
||||||
|
T: FieldReader<'t>,
|
||||||
|
{
|
||||||
|
type Future = LocalBoxFuture<'t, Result<(), MultipartError>>;
|
||||||
|
|
||||||
|
fn handle_field(
|
||||||
|
req: &'t HttpRequest,
|
||||||
|
field: Field,
|
||||||
|
limits: &'t mut Limits,
|
||||||
|
state: &'t mut State,
|
||||||
|
_duplicate_field: DuplicateField,
|
||||||
|
) -> Self::Future {
|
||||||
|
Box::pin(async move {
|
||||||
|
// Note: Vec GroupReader always allows duplicates
|
||||||
|
|
||||||
|
let field_name = field.name().to_owned();
|
||||||
|
|
||||||
|
let vec = state
|
||||||
|
.entry(field_name)
|
||||||
|
.or_insert_with(|| Box::<Vec<T>>::default())
|
||||||
|
.downcast_mut::<Vec<T>>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let item = T::read_field(req, field, limits).await?;
|
||||||
|
vec.push(item);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_state(name: &str, state: &'t mut State) -> Result<Self, MultipartError> {
|
||||||
|
Ok(state
|
||||||
|
.remove(name)
|
||||||
|
.map(|m| *m.downcast::<Vec<T>>().unwrap())
|
||||||
|
.unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t, T> FieldGroupReader<'t> for T
|
||||||
|
where
|
||||||
|
T: FieldReader<'t>,
|
||||||
|
{
|
||||||
|
type Future = LocalBoxFuture<'t, Result<(), MultipartError>>;
|
||||||
|
|
||||||
|
fn handle_field(
|
||||||
|
req: &'t HttpRequest,
|
||||||
|
field: Field,
|
||||||
|
limits: &'t mut Limits,
|
||||||
|
state: &'t mut State,
|
||||||
|
duplicate_field: DuplicateField,
|
||||||
|
) -> Self::Future {
|
||||||
|
if state.contains_key(field.name()) {
|
||||||
|
match duplicate_field {
|
||||||
|
DuplicateField::Ignore => return Box::pin(ready(Ok(()))),
|
||||||
|
|
||||||
|
DuplicateField::Deny => {
|
||||||
|
return Box::pin(ready(Err(MultipartError::DuplicateField(
|
||||||
|
field.name().to_owned(),
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
DuplicateField::Replace => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let field_name = field.name().to_owned();
|
||||||
|
let t = T::read_field(req, field, limits).await?;
|
||||||
|
state.insert(field_name, Box::new(t));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_state(name: &str, state: &'t mut State) -> Result<Self, MultipartError> {
|
||||||
|
state
|
||||||
|
.remove(name)
|
||||||
|
.map(|m| *m.downcast::<T>().unwrap())
|
||||||
|
.ok_or_else(|| MultipartError::MissingField(name.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor.
|
||||||
|
///
|
||||||
|
/// You should use the [`macro@MultipartForm`] macro to derive this for your struct.
|
||||||
|
pub trait MultipartCollect: Sized {
|
||||||
|
/// An optional limit in bytes to be applied a given field name. Note this limit will be shared
|
||||||
|
/// across all fields sharing the same name.
|
||||||
|
fn limit(field_name: &str) -> Option<usize>;
|
||||||
|
|
||||||
|
/// The extractor will call this function for each incoming field, the state can be updated
|
||||||
|
/// with the processed field data.
|
||||||
|
fn handle_field<'t>(
|
||||||
|
req: &'t HttpRequest,
|
||||||
|
field: Field,
|
||||||
|
limits: &'t mut Limits,
|
||||||
|
state: &'t mut State,
|
||||||
|
) -> LocalBoxFuture<'t, Result<(), MultipartError>>;
|
||||||
|
|
||||||
|
/// Once all the fields have been processed and stored in the state, this is called
|
||||||
|
/// to convert into the struct representation.
|
||||||
|
fn from_state(state: State) -> Result<Self, MultipartError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub enum DuplicateField {
|
||||||
|
/// Additional fields are not processed.
|
||||||
|
Ignore,
|
||||||
|
|
||||||
|
/// An error will be raised.
|
||||||
|
Deny,
|
||||||
|
|
||||||
|
/// All fields will be processed, the last one will replace all previous.
|
||||||
|
Replace,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to keep track of the remaining limits for the form and current field.
|
||||||
|
pub struct Limits {
|
||||||
|
pub total_limit_remaining: usize,
|
||||||
|
pub memory_limit_remaining: usize,
|
||||||
|
pub field_limit_remaining: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Limits {
|
||||||
|
pub fn new(total_limit: usize, memory_limit: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
total_limit_remaining: total_limit,
|
||||||
|
memory_limit_remaining: memory_limit,
|
||||||
|
field_limit_remaining: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function should be called within a [`FieldReader`] when reading each chunk of a field
|
||||||
|
/// to ensure that the form limits are not exceeded.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `bytes` - The number of bytes being read from this chunk
|
||||||
|
/// * `in_memory` - Whether to consume from the memory limits
|
||||||
|
pub fn try_consume_limits(
|
||||||
|
&mut self,
|
||||||
|
bytes: usize,
|
||||||
|
in_memory: bool,
|
||||||
|
) -> Result<(), MultipartError> {
|
||||||
|
self.total_limit_remaining = self
|
||||||
|
.total_limit_remaining
|
||||||
|
.checked_sub(bytes)
|
||||||
|
.ok_or(MultipartError::Payload(PayloadError::Overflow))?;
|
||||||
|
|
||||||
|
if in_memory {
|
||||||
|
self.memory_limit_remaining = self
|
||||||
|
.memory_limit_remaining
|
||||||
|
.checked_sub(bytes)
|
||||||
|
.ok_or(MultipartError::Payload(PayloadError::Overflow))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(field_limit) = self.field_limit_remaining {
|
||||||
|
self.field_limit_remaining = Some(
|
||||||
|
field_limit
|
||||||
|
.checked_sub(bytes)
|
||||||
|
.ok_or(MultipartError::Payload(PayloadError::Overflow))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed `multipart/form-data` extractor.
|
||||||
|
///
|
||||||
|
/// To extract typed data from a multipart stream, the inner type `T` must implement the
|
||||||
|
/// [`MultipartCollect`] trait. You should use the [`macro@MultipartForm`] macro to derive this
|
||||||
|
/// for your struct.
|
||||||
|
///
|
||||||
|
/// Add a [`MultipartFormConfig`] to your app data to configure extraction.
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct MultipartForm<T: MultipartCollect>(pub T);
|
||||||
|
|
||||||
|
impl<T: MultipartCollect> MultipartForm<T> {
|
||||||
|
/// Unwrap into inner `T` value.
|
||||||
|
pub fn into_inner(self) -> T {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> FromRequest for MultipartForm<T>
|
||||||
|
where
|
||||||
|
T: MultipartCollect,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
|
||||||
|
let mut payload = Multipart::new(req.headers(), payload.take());
|
||||||
|
|
||||||
|
let config = MultipartFormConfig::from_req(req);
|
||||||
|
let mut limits = Limits::new(config.total_limit, config.memory_limit);
|
||||||
|
|
||||||
|
let req = req.clone();
|
||||||
|
let req2 = req.clone();
|
||||||
|
let err_handler = config.err_handler.clone();
|
||||||
|
|
||||||
|
Box::pin(
|
||||||
|
async move {
|
||||||
|
let mut state = State::default();
|
||||||
|
// We need to ensure field limits are shared for all instances of this field name
|
||||||
|
let mut field_limits = HashMap::<String, Option<usize>>::new();
|
||||||
|
|
||||||
|
while let Some(field) = payload.try_next().await? {
|
||||||
|
// Retrieve the limit for this field
|
||||||
|
let entry = field_limits
|
||||||
|
.entry(field.name().to_owned())
|
||||||
|
.or_insert_with(|| T::limit(field.name()));
|
||||||
|
limits.field_limit_remaining = entry.to_owned();
|
||||||
|
|
||||||
|
T::handle_field(&req, field, &mut limits, &mut state).await?;
|
||||||
|
|
||||||
|
// Update the stored limit
|
||||||
|
*entry = limits.field_limit_remaining;
|
||||||
|
}
|
||||||
|
let inner = T::from_state(state)?;
|
||||||
|
Ok(MultipartForm(inner))
|
||||||
|
}
|
||||||
|
.map_err(move |err| {
|
||||||
|
if let Some(handler) = err_handler {
|
||||||
|
(*handler)(err, &req2)
|
||||||
|
} else {
|
||||||
|
err.into()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultipartFormErrorHandler =
|
||||||
|
Option<Arc<dyn Fn(MultipartError, &HttpRequest) -> Error + Send + Sync>>;
|
||||||
|
|
||||||
|
/// [`struct@MultipartForm`] extractor configuration.
|
||||||
|
///
|
||||||
|
/// Add to your app data to have it picked up by [`struct@MultipartForm`] extractors.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MultipartFormConfig {
|
||||||
|
total_limit: usize,
|
||||||
|
memory_limit: usize,
|
||||||
|
err_handler: MultipartFormErrorHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultipartFormConfig {
|
||||||
|
/// Sets maximum accepted payload size for the entire form. By default this limit is 50MiB.
|
||||||
|
pub fn total_limit(mut self, total_limit: usize) -> Self {
|
||||||
|
self.total_limit = total_limit;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets maximum accepted data that will be read into memory. By default this limit is 2MiB.
|
||||||
|
pub fn memory_limit(mut self, memory_limit: usize) -> Self {
|
||||||
|
self.memory_limit = memory_limit;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets custom error handler.
|
||||||
|
pub fn error_handler<F>(mut self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(MultipartError, &HttpRequest) -> Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.err_handler = Some(Arc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
|
||||||
|
/// back to the default payload config.
|
||||||
|
fn from_req(req: &HttpRequest) -> &Self {
|
||||||
|
req.app_data::<Self>()
|
||||||
|
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
|
||||||
|
.unwrap_or(&DEFAULT_CONFIG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: MultipartFormConfig = MultipartFormConfig {
|
||||||
|
total_limit: 52_428_800, // 50 MiB
|
||||||
|
memory_limit: 2_097_152, // 2 MiB
|
||||||
|
err_handler: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Default for MultipartFormConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
DEFAULT_CONFIG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use actix_http::encoding::Decoder;
|
||||||
|
use actix_multipart_rfc7578::client::multipart;
|
||||||
|
use actix_test::TestServer;
|
||||||
|
use actix_web::{dev::Payload, http::StatusCode, web, App, HttpResponse, Responder};
|
||||||
|
use awc::{Client, ClientResponse};
|
||||||
|
|
||||||
|
use super::MultipartForm;
|
||||||
|
use crate::form::{bytes::Bytes, tempfile::TempFile, text::Text, MultipartFormConfig};
|
||||||
|
|
||||||
|
pub async fn send_form(
|
||||||
|
srv: &TestServer,
|
||||||
|
form: multipart::Form<'static>,
|
||||||
|
uri: &'static str,
|
||||||
|
) -> ClientResponse<Decoder<Payload>> {
|
||||||
|
Client::default()
|
||||||
|
.post(srv.url(uri))
|
||||||
|
.content_type(form.content_type())
|
||||||
|
.send_body(multipart::Body::from(form))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test `Option` fields.
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct TestOptions {
|
||||||
|
field1: Option<Text<String>>,
|
||||||
|
field2: Option<Text<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_options_route(form: MultipartForm<TestOptions>) -> impl Responder {
|
||||||
|
assert!(form.field1.is_some());
|
||||||
|
assert!(form.field2.is_none());
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_options() {
|
||||||
|
let srv =
|
||||||
|
actix_test::start(|| App::new().route("/", web::post().to(test_options_route)));
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field1", "value");
|
||||||
|
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test `Vec` fields.
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct TestVec {
|
||||||
|
list1: Vec<Text<String>>,
|
||||||
|
list2: Vec<Text<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_vec_route(form: MultipartForm<TestVec>) -> impl Responder {
|
||||||
|
let form = form.into_inner();
|
||||||
|
let strings = form
|
||||||
|
.list1
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.into_inner())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(strings, vec!["value1", "value2", "value3"]);
|
||||||
|
assert_eq!(form.list2.len(), 0);
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_vec() {
|
||||||
|
let srv = actix_test::start(|| App::new().route("/", web::post().to(test_vec_route)));
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("list1", "value1");
|
||||||
|
form.add_text("list1", "value2");
|
||||||
|
form.add_text("list1", "value3");
|
||||||
|
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the `rename` field attribute.
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct TestFieldRenaming {
|
||||||
|
#[multipart(rename = "renamed")]
|
||||||
|
field1: Text<String>,
|
||||||
|
#[multipart(rename = "field1")]
|
||||||
|
field2: Text<String>,
|
||||||
|
field3: Text<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_field_renaming_route(
|
||||||
|
form: MultipartForm<TestFieldRenaming>,
|
||||||
|
) -> impl Responder {
|
||||||
|
assert_eq!(&*form.field1, "renamed");
|
||||||
|
assert_eq!(&*form.field2, "field1");
|
||||||
|
assert_eq!(&*form.field3, "field3");
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_field_renaming() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new().route("/", web::post().to(test_field_renaming_route))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("renamed", "renamed");
|
||||||
|
form.add_text("field1", "field1");
|
||||||
|
form.add_text("field3", "field3");
|
||||||
|
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the `deny_unknown_fields` struct attribute.
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
#[multipart(deny_unknown_fields)]
|
||||||
|
struct TestDenyUnknown {}
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct TestAllowUnknown {}
|
||||||
|
|
||||||
|
async fn test_deny_unknown_route(_: MultipartForm<TestDenyUnknown>) -> impl Responder {
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_allow_unknown_route(_: MultipartForm<TestAllowUnknown>) -> impl Responder {
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_deny_unknown() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.route("/deny", web::post().to(test_deny_unknown_route))
|
||||||
|
.route("/allow", web::post().to(test_allow_unknown_route))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("unknown", "value");
|
||||||
|
let response = send_form(&srv, form, "/deny").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("unknown", "value");
|
||||||
|
let response = send_form(&srv, form, "/allow").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the `duplicate_field` struct attribute.
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
#[multipart(duplicate_field = "deny")]
|
||||||
|
struct TestDuplicateDeny {
|
||||||
|
_field: Text<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
#[multipart(duplicate_field = "replace")]
|
||||||
|
struct TestDuplicateReplace {
|
||||||
|
field: Text<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
#[multipart(duplicate_field = "ignore")]
|
||||||
|
struct TestDuplicateIgnore {
|
||||||
|
field: Text<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_duplicate_deny_route(_: MultipartForm<TestDuplicateDeny>) -> impl Responder {
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_duplicate_replace_route(
|
||||||
|
form: MultipartForm<TestDuplicateReplace>,
|
||||||
|
) -> impl Responder {
|
||||||
|
assert_eq!(&*form.field, "second_value");
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_duplicate_ignore_route(
|
||||||
|
form: MultipartForm<TestDuplicateIgnore>,
|
||||||
|
) -> impl Responder {
|
||||||
|
assert_eq!(&*form.field, "first_value");
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_duplicate_field() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.route("/deny", web::post().to(test_duplicate_deny_route))
|
||||||
|
.route("/replace", web::post().to(test_duplicate_replace_route))
|
||||||
|
.route("/ignore", web::post().to(test_duplicate_ignore_route))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("_field", "first_value");
|
||||||
|
form.add_text("_field", "second_value");
|
||||||
|
let response = send_form(&srv, form, "/deny").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "first_value");
|
||||||
|
form.add_text("field", "second_value");
|
||||||
|
let response = send_form(&srv, form, "/replace").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "first_value");
|
||||||
|
form.add_text("field", "second_value");
|
||||||
|
let response = send_form(&srv, form, "/ignore").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the Limits.
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct TestMemoryUploadLimits {
|
||||||
|
field: Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct TestFileUploadLimits {
|
||||||
|
field: TempFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_upload_limits_memory(
|
||||||
|
form: MultipartForm<TestMemoryUploadLimits>,
|
||||||
|
) -> impl Responder {
|
||||||
|
assert!(!form.field.data.is_empty());
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_upload_limits_file(
|
||||||
|
form: MultipartForm<TestFileUploadLimits>,
|
||||||
|
) -> impl Responder {
|
||||||
|
assert!(form.field.size > 0);
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_memory_limits() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.route("/text", web::post().to(test_upload_limits_memory))
|
||||||
|
.route("/file", web::post().to(test_upload_limits_file))
|
||||||
|
.app_data(
|
||||||
|
MultipartFormConfig::default()
|
||||||
|
.memory_limit(20)
|
||||||
|
.total_limit(usize::MAX),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exceeds the 20 byte memory limit
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "this string is 28 bytes long");
|
||||||
|
let response = send_form(&srv, form, "/text").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
// Memory limit should not apply when the data is being streamed to disk
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "this string is 28 bytes long");
|
||||||
|
let response = send_form(&srv, form, "/file").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_total_limit() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.route("/text", web::post().to(test_upload_limits_memory))
|
||||||
|
.route("/file", web::post().to(test_upload_limits_file))
|
||||||
|
.app_data(
|
||||||
|
MultipartFormConfig::default()
|
||||||
|
.memory_limit(usize::MAX)
|
||||||
|
.total_limit(20),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Within the 20 byte limit
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "7 bytes");
|
||||||
|
let response = send_form(&srv, form, "/text").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// Exceeds the 20 byte overall limit
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "this string is 28 bytes long");
|
||||||
|
let response = send_form(&srv, form, "/text").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
// Exceeds the 20 byte overall limit
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "this string is 28 bytes long");
|
||||||
|
let response = send_form(&srv, form, "/file").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct TestFieldLevelLimits {
|
||||||
|
#[multipart(limit = "30B")]
|
||||||
|
field: Vec<Bytes>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_field_level_limits_route(
|
||||||
|
form: MultipartForm<TestFieldLevelLimits>,
|
||||||
|
) -> impl Responder {
|
||||||
|
assert!(!form.field.is_empty());
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_field_level_limits() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.route("/", web::post().to(test_field_level_limits_route))
|
||||||
|
.app_data(
|
||||||
|
MultipartFormConfig::default()
|
||||||
|
.memory_limit(usize::MAX)
|
||||||
|
.total_limit(usize::MAX),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Within the 30 byte limit
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "this string is 28 bytes long");
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// Exceeds the the 30 byte limit
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "this string is more than 30 bytes long");
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
// Total of values (14 bytes) is within 30 byte limit for "field"
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "7 bytes");
|
||||||
|
form.add_text("field", "7 bytes");
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// Total of values exceeds 30 byte limit for "field"
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_text("field", "this string is 28 bytes long");
|
||||||
|
form.add_text("field", "this string is 28 bytes long");
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
//! Writes a field to a temporary file on disk.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError};
|
||||||
|
use derive_more::{Display, Error};
|
||||||
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
use futures_util::TryStreamExt as _;
|
||||||
|
use mime::Mime;
|
||||||
|
use tempfile_dep::NamedTempFile;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
use super::FieldErrorHandler;
|
||||||
|
use crate::{
|
||||||
|
form::{FieldReader, Limits},
|
||||||
|
Field, MultipartError,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Write the field to a temporary file on disk.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TempFile {
|
||||||
|
/// The temporary file on disk.
|
||||||
|
pub file: NamedTempFile,
|
||||||
|
|
||||||
|
/// The value of the `content-type` header.
|
||||||
|
pub content_type: Option<Mime>,
|
||||||
|
|
||||||
|
/// The `filename` value in the `content-disposition` header.
|
||||||
|
pub file_name: Option<String>,
|
||||||
|
|
||||||
|
/// The size in bytes of the file.
|
||||||
|
pub size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> FieldReader<'t> for TempFile {
|
||||||
|
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
|
||||||
|
|
||||||
|
fn read_field(
|
||||||
|
req: &'t HttpRequest,
|
||||||
|
mut field: Field,
|
||||||
|
limits: &'t mut Limits,
|
||||||
|
) -> Self::Future {
|
||||||
|
Box::pin(async move {
|
||||||
|
let config = TempFileConfig::from_req(req);
|
||||||
|
let field_name = field.name().to_owned();
|
||||||
|
let mut size = 0;
|
||||||
|
|
||||||
|
let file = config.create_tempfile().map_err(|err| {
|
||||||
|
config.map_error(req, &field_name, TempFileError::FileIo(err))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut file_async = tokio::fs::File::from_std(file.reopen().map_err(|err| {
|
||||||
|
config.map_error(req, &field_name, TempFileError::FileIo(err))
|
||||||
|
})?);
|
||||||
|
|
||||||
|
while let Some(chunk) = field.try_next().await? {
|
||||||
|
limits.try_consume_limits(chunk.len(), false)?;
|
||||||
|
size += chunk.len();
|
||||||
|
file_async.write_all(chunk.as_ref()).await.map_err(|err| {
|
||||||
|
config.map_error(req, &field_name, TempFileError::FileIo(err))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_async.flush().await.map_err(|err| {
|
||||||
|
config.map_error(req, &field_name, TempFileError::FileIo(err))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(TempFile {
|
||||||
|
file,
|
||||||
|
content_type: field.content_type().map(ToOwned::to_owned),
|
||||||
|
file_name: field
|
||||||
|
.content_disposition()
|
||||||
|
.get_filename()
|
||||||
|
.map(str::to_owned),
|
||||||
|
size,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum TempFileError {
|
||||||
|
/// File I/O Error
|
||||||
|
#[display(fmt = "File I/O error: {}", _0)]
|
||||||
|
FileIo(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for TempFileError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the [`TempFile`] field reader.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TempFileConfig {
|
||||||
|
err_handler: FieldErrorHandler<TempFileError>,
|
||||||
|
directory: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempFileConfig {
|
||||||
|
fn create_tempfile(&self) -> io::Result<NamedTempFile> {
|
||||||
|
if let Some(ref dir) = self.directory {
|
||||||
|
NamedTempFile::new_in(dir)
|
||||||
|
} else {
|
||||||
|
NamedTempFile::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempFileConfig {
|
||||||
|
/// Sets custom error handler.
|
||||||
|
pub fn error_handler<F>(mut self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(TempFileError, &HttpRequest) -> Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.err_handler = Some(Arc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
|
||||||
|
/// back to the default payload config.
|
||||||
|
fn from_req(req: &HttpRequest) -> &Self {
|
||||||
|
req.app_data::<Self>()
|
||||||
|
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
|
||||||
|
.unwrap_or(&DEFAULT_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_error(
|
||||||
|
&self,
|
||||||
|
req: &HttpRequest,
|
||||||
|
field_name: &str,
|
||||||
|
err: TempFileError,
|
||||||
|
) -> MultipartError {
|
||||||
|
let source = if let Some(ref err_handler) = self.err_handler {
|
||||||
|
(err_handler)(err, req)
|
||||||
|
} else {
|
||||||
|
err.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
MultipartError::Field {
|
||||||
|
field_name: field_name.to_owned(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the directory that temp files will be created in.
|
||||||
|
///
|
||||||
|
/// The default temporary file location is platform dependent.
|
||||||
|
pub fn directory(mut self, dir: impl AsRef<Path>) -> Self {
|
||||||
|
self.directory = Some(dir.as_ref().to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: TempFileConfig = TempFileConfig {
|
||||||
|
err_handler: None,
|
||||||
|
directory: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Default for TempFileConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
DEFAULT_CONFIG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
|
||||||
|
use actix_multipart_rfc7578::client::multipart;
|
||||||
|
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
|
||||||
|
|
||||||
|
use crate::form::{tempfile::TempFile, tests::send_form, MultipartForm};
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct FileForm {
|
||||||
|
file: TempFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_file_route(form: MultipartForm<FileForm>) -> impl Responder {
|
||||||
|
let mut form = form.into_inner();
|
||||||
|
let mut contents = String::new();
|
||||||
|
form.file.file.read_to_string(&mut contents).unwrap();
|
||||||
|
assert_eq!(contents, "Hello, world!");
|
||||||
|
assert_eq!(form.file.file_name.unwrap(), "testfile.txt");
|
||||||
|
assert_eq!(form.file.content_type.unwrap(), mime::TEXT_PLAIN);
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_file_upload() {
|
||||||
|
let srv = actix_test::start(|| App::new().route("/", web::post().to(test_file_route)));
|
||||||
|
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
let bytes = Cursor::new("Hello, world!");
|
||||||
|
form.add_reader_file_with_mime("file", bytes, "testfile.txt", mime::TEXT_PLAIN);
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
//! Deserializes a field from plain text.
|
||||||
|
|
||||||
|
use std::{str, sync::Arc};
|
||||||
|
|
||||||
|
use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError};
|
||||||
|
use derive_more::{Deref, DerefMut, Display, Error};
|
||||||
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
use super::FieldErrorHandler;
|
||||||
|
use crate::{
|
||||||
|
form::{bytes::Bytes, FieldReader, Limits},
|
||||||
|
Field, MultipartError,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Deserialize from plain text.
|
||||||
|
///
|
||||||
|
/// Internally this uses [`serde_plain`] for deserialization, which supports primitive types
|
||||||
|
/// including strings, numbers, and simple enums.
|
||||||
|
#[derive(Debug, Deref, DerefMut)]
|
||||||
|
pub struct Text<T: DeserializeOwned>(pub T);
|
||||||
|
|
||||||
|
impl<T: DeserializeOwned> Text<T> {
|
||||||
|
/// Unwraps into inner value.
|
||||||
|
pub fn into_inner(self) -> T {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t, T> FieldReader<'t> for Text<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + 'static,
|
||||||
|
{
|
||||||
|
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
|
||||||
|
|
||||||
|
fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future {
|
||||||
|
Box::pin(async move {
|
||||||
|
let config = TextConfig::from_req(req);
|
||||||
|
let field_name = field.name().to_owned();
|
||||||
|
|
||||||
|
if config.validate_content_type {
|
||||||
|
let valid = if let Some(mime) = field.content_type() {
|
||||||
|
mime.subtype() == mime::PLAIN || mime.suffix() == Some(mime::PLAIN)
|
||||||
|
} else {
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7578#section-4.4
|
||||||
|
// content type defaults to text/plain, so None should be considered valid
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return Err(MultipartError::Field {
|
||||||
|
field_name,
|
||||||
|
source: config.map_error(req, TextError::ContentType),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = Bytes::read_field(req, field, limits).await?;
|
||||||
|
|
||||||
|
let text = str::from_utf8(&bytes.data).map_err(|err| MultipartError::Field {
|
||||||
|
field_name: field_name.clone(),
|
||||||
|
source: config.map_error(req, TextError::Utf8Error(err)),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Text(serde_plain::from_str(text).map_err(|err| {
|
||||||
|
MultipartError::Field {
|
||||||
|
field_name,
|
||||||
|
source: config.map_error(req, TextError::Deserialize(err)),
|
||||||
|
}
|
||||||
|
})?))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum TextError {
|
||||||
|
/// UTF-8 decoding error.
|
||||||
|
#[display(fmt = "UTF-8 decoding error: {}", _0)]
|
||||||
|
Utf8Error(str::Utf8Error),
|
||||||
|
|
||||||
|
/// Deserialize error.
|
||||||
|
#[display(fmt = "Plain text deserialize error: {}", _0)]
|
||||||
|
Deserialize(serde_plain::Error),
|
||||||
|
|
||||||
|
/// Content type error.
|
||||||
|
#[display(fmt = "Content type error")]
|
||||||
|
ContentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for TextError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the [`Text`] field reader.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TextConfig {
|
||||||
|
err_handler: FieldErrorHandler<TextError>,
|
||||||
|
validate_content_type: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextConfig {
|
||||||
|
/// Sets custom error handler.
|
||||||
|
pub fn error_handler<F>(mut self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(TextError, &HttpRequest) -> Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.err_handler = Some(Arc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
|
||||||
|
/// back to the default payload config.
|
||||||
|
fn from_req(req: &HttpRequest) -> &Self {
|
||||||
|
req.app_data::<Self>()
|
||||||
|
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
|
||||||
|
.unwrap_or(&DEFAULT_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_error(&self, req: &HttpRequest, err: TextError) -> Error {
|
||||||
|
if let Some(ref err_handler) = self.err_handler {
|
||||||
|
(err_handler)(err, req)
|
||||||
|
} else {
|
||||||
|
err.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether or not the field must have a valid `Content-Type` header to be parsed.
|
||||||
|
///
|
||||||
|
/// Note that an empty `Content-Type` is also accepted, as the multipart specification defines
|
||||||
|
/// `text/plain` as the default for text fields.
|
||||||
|
pub fn validate_content_type(mut self, validate_content_type: bool) -> Self {
|
||||||
|
self.validate_content_type = validate_content_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: TextConfig = TextConfig {
|
||||||
|
err_handler: None,
|
||||||
|
validate_content_type: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Default for TextConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
DEFAULT_CONFIG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use actix_multipart_rfc7578::client::multipart;
|
||||||
|
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
|
||||||
|
|
||||||
|
use crate::form::{
|
||||||
|
tests::send_form,
|
||||||
|
text::{Text, TextConfig},
|
||||||
|
MultipartForm,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(MultipartForm)]
|
||||||
|
struct TextForm {
|
||||||
|
number: Text<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_text_route(form: MultipartForm<TextForm>) -> impl Responder {
|
||||||
|
assert_eq!(*form.number, 1025);
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_content_type_validation() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.route("/", web::post().to(test_text_route))
|
||||||
|
.app_data(TextConfig::default().validate_content_type(true))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deny because wrong content type
|
||||||
|
let bytes = Cursor::new("1025");
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_reader_file_with_mime("number", bytes, "", mime::APPLICATION_OCTET_STREAM);
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
// Allow because correct content type
|
||||||
|
let bytes = Cursor::new("1025");
|
||||||
|
let mut form = multipart::Form::default();
|
||||||
|
form.add_reader_file_with_mime("number", bytes, "", mime::TEXT_PLAIN);
|
||||||
|
let response = send_form(&srv, form, "/").await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,18 @@
|
||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
#![allow(clippy::borrow_interior_mutable_const)]
|
#![allow(clippy::borrow_interior_mutable_const, clippy::uninlined_format_args)]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
// This allows us to use the actix_multipart_derive within this crate's tests
|
||||||
|
#[cfg(test)]
|
||||||
|
extern crate self as actix_multipart;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod extractor;
|
mod extractor;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
|
pub mod form;
|
||||||
|
|
||||||
pub use self::error::MultipartError;
|
pub use self::error::MultipartError;
|
||||||
pub use self::server::{Field, Multipart};
|
pub use self::server::{Field, Multipart};
|
||||||
|
|
|
@ -270,7 +270,9 @@ impl InnerMultipart {
|
||||||
match field.borrow_mut().poll(safety) {
|
match field.borrow_mut().poll(safety) {
|
||||||
Poll::Pending => return Poll::Pending,
|
Poll::Pending => return Poll::Pending,
|
||||||
Poll::Ready(Some(Ok(_))) => continue,
|
Poll::Ready(Some(Ok(_))) => continue,
|
||||||
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
|
Poll::Ready(Some(Err(err))) => {
|
||||||
|
return Poll::Ready(Some(Err(err)))
|
||||||
|
}
|
||||||
Poll::Ready(None) => true,
|
Poll::Ready(None) => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -658,7 +660,7 @@ impl InnerField {
|
||||||
match res {
|
match res {
|
||||||
Poll::Pending => return Poll::Pending,
|
Poll::Pending => return Poll::Pending,
|
||||||
Poll::Ready(Some(Ok(bytes))) => return Poll::Ready(Some(Ok(bytes))),
|
Poll::Ready(Some(Ok(bytes))) => return Poll::Ready(Some(Ok(bytes))),
|
||||||
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
|
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
|
||||||
Poll::Ready(None) => self.eof = true,
|
Poll::Ready(None) => self.eof = true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -673,7 +675,7 @@ impl InnerField {
|
||||||
}
|
}
|
||||||
Poll::Ready(None)
|
Poll::Ready(None)
|
||||||
}
|
}
|
||||||
Err(e) => Poll::Ready(Some(Err(e))),
|
Err(err) => Poll::Ready(Some(Err(err))),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Poll::Pending
|
Poll::Pending
|
||||||
|
@ -794,7 +796,7 @@ impl PayloadBuffer {
|
||||||
loop {
|
loop {
|
||||||
match Pin::new(&mut self.stream).poll_next(cx) {
|
match Pin::new(&mut self.stream).poll_next(cx) {
|
||||||
Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data),
|
Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data),
|
||||||
Poll::Ready(Some(Err(e))) => return Err(e),
|
Poll::Ready(Some(Err(err))) => return Err(err),
|
||||||
Poll::Ready(None) => {
|
Poll::Ready(None) => {
|
||||||
self.eof = true;
|
self.eof = true;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -860,19 +862,22 @@ impl PayloadBuffer {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use std::time::Duration;
|
||||||
|
|
||||||
use actix_http::h1::Payload;
|
use actix_http::h1;
|
||||||
use actix_web::http::header::{DispositionParam, DispositionType};
|
use actix_web::{
|
||||||
use actix_web::rt;
|
http::header::{DispositionParam, DispositionType},
|
||||||
use actix_web::test::TestRequest;
|
rt,
|
||||||
use actix_web::FromRequest;
|
test::TestRequest,
|
||||||
|
FromRequest,
|
||||||
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::{future::lazy, StreamExt as _};
|
use futures_util::{future::lazy, StreamExt as _};
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_boundary() {
|
async fn test_boundary() {
|
||||||
let headers = HeaderMap::new();
|
let headers = HeaderMap::new();
|
||||||
|
@ -1119,7 +1124,7 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_basic() {
|
async fn test_basic() {
|
||||||
let (_, payload) = Payload::create(false);
|
let (_, payload) = h1::Payload::create(false);
|
||||||
let mut payload = PayloadBuffer::new(payload);
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
assert_eq!(payload.buf.len(), 0);
|
assert_eq!(payload.buf.len(), 0);
|
||||||
|
@ -1129,7 +1134,7 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_eof() {
|
async fn test_eof() {
|
||||||
let (mut sender, payload) = Payload::create(false);
|
let (mut sender, payload) = h1::Payload::create(false);
|
||||||
let mut payload = PayloadBuffer::new(payload);
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
assert_eq!(None, payload.read_max(4).unwrap());
|
assert_eq!(None, payload.read_max(4).unwrap());
|
||||||
|
@ -1145,7 +1150,7 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_err() {
|
async fn test_err() {
|
||||||
let (mut sender, payload) = Payload::create(false);
|
let (mut sender, payload) = h1::Payload::create(false);
|
||||||
let mut payload = PayloadBuffer::new(payload);
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
assert_eq!(None, payload.read_max(1).unwrap());
|
assert_eq!(None, payload.read_max(1).unwrap());
|
||||||
sender.set_error(PayloadError::Incomplete(None));
|
sender.set_error(PayloadError::Incomplete(None));
|
||||||
|
@ -1154,7 +1159,7 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_readmax() {
|
async fn test_readmax() {
|
||||||
let (mut sender, payload) = Payload::create(false);
|
let (mut sender, payload) = h1::Payload::create(false);
|
||||||
let mut payload = PayloadBuffer::new(payload);
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
sender.feed_data(Bytes::from("line1"));
|
sender.feed_data(Bytes::from("line1"));
|
||||||
|
@ -1171,7 +1176,7 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_readexactly() {
|
async fn test_readexactly() {
|
||||||
let (mut sender, payload) = Payload::create(false);
|
let (mut sender, payload) = h1::Payload::create(false);
|
||||||
let mut payload = PayloadBuffer::new(payload);
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
assert_eq!(None, payload.read_exact(2));
|
assert_eq!(None, payload.read_exact(2));
|
||||||
|
@ -1189,7 +1194,7 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_readuntil() {
|
async fn test_readuntil() {
|
||||||
let (mut sender, payload) = Payload::create(false);
|
let (mut sender, payload) = h1::Payload::create(false);
|
||||||
let mut payload = PayloadBuffer::new(payload);
|
let mut payload = PayloadBuffer::new(payload);
|
||||||
|
|
||||||
assert_eq!(None, payload.read_until(b"ne").unwrap());
|
assert_eq!(None, payload.read_until(b"ne").unwrap());
|
||||||
|
@ -1230,7 +1235,7 @@ mod tests {
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_multipart_payload_consumption() {
|
async fn test_multipart_payload_consumption() {
|
||||||
// with sample payload and HttpRequest with no headers
|
// with sample payload and HttpRequest with no headers
|
||||||
let (_, inner_payload) = Payload::create(false);
|
let (_, inner_payload) = h1::Payload::create(false);
|
||||||
let mut payload = actix_web::dev::Payload::from(inner_payload);
|
let mut payload = actix_web::dev::Payload::from(inner_payload);
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,17 @@
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
|
||||||
## 0.5.1 - 2022-09-19
|
## 0.5.1 - 2022-09-19
|
||||||
|
|
||||||
- Correct typo in error string for `i32` deserialization. [#2876]
|
- Correct typo in error string for `i32` deserialization. [#2876]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||||
|
|
||||||
[#2876]: https://github.com/actix/actix-web/pull/2876
|
[#2876]: https://github.com/actix/actix-web/pull/2876
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0 - 2022-02-22
|
## 0.5.0 - 2022-02-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Add `Path::as_str`. [#2590]
|
- Add `Path::as_str`. [#2590]
|
||||||
- Add `ResourceDef::set_name`. [#373][net#373]
|
- Add `ResourceDef::set_name`. [#373][net#373]
|
||||||
- Add `RouterBuilder::push`. [#2612]
|
- Add `RouterBuilder::push`. [#2612]
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
- Support multi-pattern prefixes and joins. [#2356]
|
- Support multi-pattern prefixes and joins. [#2356]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612]
|
- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612]
|
||||||
- Deprecate `Path::path`. [#2590]
|
- Deprecate `Path::path`. [#2590]
|
||||||
- Disallow prefix routes with tail segments. [#379][net#379]
|
- Disallow prefix routes with tail segments. [#379][net#379]
|
||||||
|
@ -47,6 +49,7 @@
|
||||||
- Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373]
|
- Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix `ResourceDef`'s `PartialEq` implementation. [#373][net#373]
|
- Fix `ResourceDef`'s `PartialEq` implementation. [#373][net#373]
|
||||||
- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368]
|
- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368]
|
||||||
- Improve malformed path error message. [#384][net#384]
|
- Improve malformed path error message. [#384][net#384]
|
||||||
|
@ -55,6 +58,7 @@
|
||||||
- Static patterns in multi-patterns are no longer interpreted as regex. [#366][net#366]
|
- Static patterns in multi-patterns are no longer interpreted as regex. [#366][net#366]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- `ResourceDef::name_mut`. [#373][net#373]
|
- `ResourceDef::name_mut`. [#373][net#373]
|
||||||
- Unused `ResourceInfo`. [#2612]
|
- Unused `ResourceInfo`. [#2612]
|
||||||
|
|
||||||
|
@ -77,11 +81,11 @@
|
||||||
[net#380]: https://github.com/actix/actix-net/pull/380
|
[net#380]: https://github.com/actix/actix-net/pull/380
|
||||||
[net#384]: https://github.com/actix/actix-net/pull/384
|
[net#384]: https://github.com/actix/actix-net/pull/384
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>0.5.0 Pre-Releases</summary>
|
<summary>0.5.0 Pre-Releases</summary>
|
||||||
|
|
||||||
## 0.5.0-rc.3 - 2022-01-31
|
## 0.5.0-rc.3 - 2022-01-31
|
||||||
|
|
||||||
- Remove unused `ResourceInfo`. [#2612]
|
- Remove unused `ResourceInfo`. [#2612]
|
||||||
- Add `RouterBuilder::push`. [#2612]
|
- Add `RouterBuilder::push`. [#2612]
|
||||||
- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612]
|
- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612]
|
||||||
|
@ -92,33 +96,33 @@
|
||||||
[#2612]: https://github.com/actix/actix-web/pull/2612
|
[#2612]: https://github.com/actix/actix-web/pull/2612
|
||||||
[#2613]: https://github.com/actix/actix-web/pull/2613
|
[#2613]: https://github.com/actix/actix-web/pull/2613
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0-rc.2 - 2022-01-21
|
## 0.5.0-rc.2 - 2022-01-21
|
||||||
|
|
||||||
- Add `Path::as_str`. [#2590]
|
- Add `Path::as_str`. [#2590]
|
||||||
- Deprecate `Path::path`. [#2590]
|
- Deprecate `Path::path`. [#2590]
|
||||||
|
|
||||||
[#2590]: https://github.com/actix/actix-web/pull/2590
|
[#2590]: https://github.com/actix/actix-web/pull/2590
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0-rc.1 - 2022-01-14
|
## 0.5.0-rc.1 - 2022-01-14
|
||||||
|
|
||||||
- `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568]
|
- `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568]
|
||||||
- `Resource` is now implemented for `&mut Path<_>` and `RefMut<Path<_>>`. [#2568]
|
- `Resource` is now implemented for `&mut Path<_>` and `RefMut<Path<_>>`. [#2568]
|
||||||
|
|
||||||
[#2568]: https://github.com/actix/actix-web/pull/2568
|
[#2568]: https://github.com/actix/actix-web/pull/2568
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0-beta.4 - 2022-01-04
|
## 0.5.0-beta.4 - 2022-01-04
|
||||||
|
|
||||||
- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566]
|
- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
[#2566]: https://github.com/actix/actix-net/pull/2566
|
[#2566]: https://github.com/actix/actix-net/pull/2566
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0-beta.3 - 2021-12-17
|
## 0.5.0-beta.3 - 2021-12-17
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0-beta.2 - 2021-09-09
|
## 0.5.0-beta.2 - 2021-09-09
|
||||||
|
|
||||||
- Introduce `ResourceDef::join`. [#380][net#380]
|
- Introduce `ResourceDef::join`. [#380][net#380]
|
||||||
- Disallow prefix routes with tail segments. [#379][net#379]
|
- Disallow prefix routes with tail segments. [#379][net#379]
|
||||||
- Enforce path separators on dynamic prefixes. [#378][net#378]
|
- Enforce path separators on dynamic prefixes. [#378][net#378]
|
||||||
|
@ -137,8 +141,8 @@
|
||||||
[#2355]: https://github.com/actix/actix-web/pull/2355
|
[#2355]: https://github.com/actix/actix-web/pull/2355
|
||||||
[#2356]: https://github.com/actix/actix-web/pull/2356
|
[#2356]: https://github.com/actix/actix-web/pull/2356
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0-beta.1 - 2021-07-20
|
## 0.5.0-beta.1 - 2021-07-20
|
||||||
|
|
||||||
- Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366][net#366]
|
- Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366][net#366]
|
||||||
- Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373]
|
- Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373]
|
||||||
- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368]
|
- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368]
|
||||||
|
@ -167,8 +171,8 @@
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0 - 2021-06-06
|
## 0.4.0 - 2021-06-06
|
||||||
|
|
||||||
- When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357][net#357]
|
- When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357][net#357]
|
||||||
- Path tail patterns now match new lines (`\n`) in request URL. [#360][net#360]
|
- Path tail patterns now match new lines (`\n`) in request URL. [#360][net#360]
|
||||||
- Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359][net#359]
|
- Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359][net#359]
|
||||||
|
@ -179,70 +183,70 @@
|
||||||
[net#359]: https://github.com/actix/actix-net/pull/359
|
[net#359]: https://github.com/actix/actix-net/pull/359
|
||||||
[net#360]: https://github.com/actix/actix-net/pull/360
|
[net#360]: https://github.com/actix/actix-net/pull/360
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0 - 2019-12-31
|
## 0.3.0 - 2019-12-31
|
||||||
|
|
||||||
- Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0
|
- Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0
|
||||||
|
|
||||||
|
|
||||||
## 0.2.7 - 2021-02-06
|
## 0.2.7 - 2021-02-06
|
||||||
|
|
||||||
- Add `Router::recognize_checked` [#247][net#247]
|
- Add `Router::recognize_checked` [#247][net#247]
|
||||||
|
|
||||||
[net#247]: https://github.com/actix/actix-net/pull/247
|
[net#247]: https://github.com/actix/actix-net/pull/247
|
||||||
|
|
||||||
|
|
||||||
## 0.2.6 - 2021-01-09
|
## 0.2.6 - 2021-01-09
|
||||||
|
|
||||||
- Use `bytestring` version range compatible with Bytes v1.0. [#246][net#246]
|
- Use `bytestring` version range compatible with Bytes v1.0. [#246][net#246]
|
||||||
|
|
||||||
[net#246]: https://github.com/actix/actix-net/pull/246
|
[net#246]: https://github.com/actix/actix-net/pull/246
|
||||||
|
|
||||||
|
|
||||||
## 0.2.5 - 2020-09-20
|
## 0.2.5 - 2020-09-20
|
||||||
|
|
||||||
- Fix `from_hex()` method
|
- Fix `from_hex()` method
|
||||||
|
|
||||||
|
|
||||||
## 0.2.4 - 2019-12-31
|
## 0.2.4 - 2019-12-31
|
||||||
|
|
||||||
- Add `ResourceDef::resource_path_named()` path generation method
|
- Add `ResourceDef::resource_path_named()` path generation method
|
||||||
|
|
||||||
|
|
||||||
## 0.2.3 - 2019-12-25
|
## 0.2.3 - 2019-12-25
|
||||||
|
|
||||||
- Add impl `IntoPattern` for `&String`
|
- Add impl `IntoPattern` for `&String`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.2 - 2019-12-25
|
## 0.2.2 - 2019-12-25
|
||||||
|
|
||||||
- Use `IntoPattern` for `RouterBuilder::path()`
|
- Use `IntoPattern` for `RouterBuilder::path()`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.1 - 2019-12-25
|
## 0.2.1 - 2019-12-25
|
||||||
|
|
||||||
- Add `IntoPattern` trait
|
- Add `IntoPattern` trait
|
||||||
- Add multi-pattern resources
|
- Add multi-pattern resources
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0 - 2019-12-07
|
## 0.2.0 - 2019-12-07
|
||||||
|
|
||||||
- Update http to 0.2
|
- Update http to 0.2
|
||||||
- Update regex to 1.3
|
- Update regex to 1.3
|
||||||
- Use bytestring instead of string
|
- Use bytestring instead of string
|
||||||
|
|
||||||
|
|
||||||
## 0.1.5 - 2019-05-15
|
## 0.1.5 - 2019-05-15
|
||||||
|
|
||||||
- Remove debug prints
|
- Remove debug prints
|
||||||
|
|
||||||
|
|
||||||
## 0.1.4 - 2019-05-15
|
## 0.1.4 - 2019-05-15
|
||||||
|
|
||||||
- Fix checked resource match
|
- Fix checked resource match
|
||||||
|
|
||||||
|
|
||||||
## 0.1.3 - 2019-04-22
|
## 0.1.3 - 2019-04-22
|
||||||
- Added support for `remainder match` (i.e "/path/{tail}*")
|
|
||||||
|
|
||||||
|
- Added support for `remainder match` (i.e "/path/{tail}\*")
|
||||||
|
|
||||||
## 0.1.2 - 2019-04-07
|
## 0.1.2 - 2019-04-07
|
||||||
|
|
||||||
- Export `Quoter` type
|
- Export `Quoter` type
|
||||||
- Allow to reset `Path` instance
|
- Allow to reset `Path` instance
|
||||||
|
|
||||||
|
|
||||||
## 0.1.1 - 2019-04-03
|
## 0.1.1 - 2019-04-03
|
||||||
|
|
||||||
- Get dynamic segment by name instead of iterator.
|
- Get dynamic segment by name instead of iterator.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0 - 2019-03-09
|
## 0.1.0 - 2019-03-09
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
#![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")]
|
||||||
|
|
||||||
|
|
|
@ -1,72 +1,76 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
## 0.1.1 - 2023-02-26
|
||||||
|
|
||||||
|
- Add `TestServerConfig::port()` setter method.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0 - 2022-07-24
|
## 0.1.0 - 2022-07-24
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.13 - 2022-02-16
|
## 0.1.0-beta.13 - 2022-02-16
|
||||||
|
|
||||||
- No significant changes since `0.1.0-beta.12`.
|
- No significant changes since `0.1.0-beta.12`.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.12 - 2022-01-31
|
## 0.1.0-beta.12 - 2022-01-31
|
||||||
|
|
||||||
- Rename `TestServerConfig::{client_timeout => client_request_timeout}`. [#2611]
|
- Rename `TestServerConfig::{client_timeout => client_request_timeout}`. [#2611]
|
||||||
|
|
||||||
[#2611]: https://github.com/actix/actix-web/pull/2611
|
[#2611]: https://github.com/actix/actix-web/pull/2611
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.11 - 2022-01-04
|
## 0.1.0-beta.11 - 2022-01-04
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.10 - 2021-12-27
|
## 0.1.0-beta.10 - 2021-12-27
|
||||||
|
|
||||||
- No significant changes since `0.1.0-beta.9`.
|
- No significant changes since `0.1.0-beta.9`.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.9 - 2021-12-17
|
## 0.1.0-beta.9 - 2021-12-17
|
||||||
|
|
||||||
- Re-export `actix_http::body::to_bytes`. [#2518]
|
- Re-export `actix_http::body::to_bytes`. [#2518]
|
||||||
- Update `actix_web::test` re-exports. [#2518]
|
- Update `actix_web::test` re-exports. [#2518]
|
||||||
|
|
||||||
[#2518]: https://github.com/actix/actix-web/pull/2518
|
[#2518]: https://github.com/actix/actix-web/pull/2518
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.8 - 2021-12-11
|
## 0.1.0-beta.8 - 2021-12-11
|
||||||
|
|
||||||
- No significant changes since `0.1.0-beta.7`.
|
- No significant changes since `0.1.0-beta.7`.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.7 - 2021-11-22
|
## 0.1.0-beta.7 - 2021-11-22
|
||||||
|
|
||||||
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||||
|
|
||||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.6 - 2021-11-15
|
## 0.1.0-beta.6 - 2021-11-15
|
||||||
|
|
||||||
- No significant changes from `0.1.0-beta.5`.
|
- No significant changes from `0.1.0-beta.5`.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.5 - 2021-10-20
|
## 0.1.0-beta.5 - 2021-10-20
|
||||||
|
|
||||||
- Updated rustls to v0.20. [#2414]
|
- Updated rustls to v0.20. [#2414]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
[#2414]: https://github.com/actix/actix-web/pull/2414
|
[#2414]: https://github.com/actix/actix-web/pull/2414
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.4 - 2021-09-09
|
## 0.1.0-beta.4 - 2021-09-09
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.3 - 2021-06-20
|
## 0.1.0-beta.3 - 2021-06-20
|
||||||
|
|
||||||
- No significant changes from `0.1.0-beta.2`.
|
- No significant changes from `0.1.0-beta.2`.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.2 - 2021-04-17
|
## 0.1.0-beta.2 - 2021-04-17
|
||||||
|
|
||||||
- No significant changes from `0.1.0-beta.1`.
|
- No significant changes from `0.1.0-beta.1`.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.1 - 2021-04-02
|
## 0.1.0-beta.1 - 2021-04-02
|
||||||
|
|
||||||
- Move integration testing structs from `actix-web`. [#2112]
|
- Move integration testing structs from `actix-web`. [#2112]
|
||||||
|
|
||||||
[#2112]: https://github.com/actix/actix-web/pull/2112
|
[#2112]: https://github.com/actix/actix-web/pull/2112
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-test"
|
name = "actix-test"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
@ -45,4 +45,4 @@ serde_json = "1"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||||
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
||||||
tokio = { version = "1.8.4", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
|
|
|
@ -145,7 +145,7 @@ where
|
||||||
// run server in separate orphaned thread
|
// run server in separate orphaned thread
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
rt::System::new().block_on(async move {
|
rt::System::new().block_on(async move {
|
||||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
let tcp = net::TcpListener::bind(("127.0.0.1", cfg.port)).unwrap();
|
||||||
let local_addr = tcp.local_addr().unwrap();
|
let local_addr = tcp.local_addr().unwrap();
|
||||||
let factory = factory.clone();
|
let factory = factory.clone();
|
||||||
let srv_cfg = cfg.clone();
|
let srv_cfg = cfg.clone();
|
||||||
|
@ -321,6 +321,7 @@ where
|
||||||
// all thread managed resources should be dropped at this point
|
// all thread managed resources should be dropped at this point
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[allow(clippy::let_underscore_future)]
|
||||||
let _ = thread_stop_tx.send(());
|
let _ = thread_stop_tx.send(());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -389,6 +390,7 @@ pub struct TestServerConfig {
|
||||||
tp: HttpVer,
|
tp: HttpVer,
|
||||||
stream: StreamType,
|
stream: StreamType,
|
||||||
client_request_timeout: Duration,
|
client_request_timeout: Duration,
|
||||||
|
port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TestServerConfig {
|
impl Default for TestServerConfig {
|
||||||
|
@ -404,6 +406,7 @@ impl TestServerConfig {
|
||||||
tp: HttpVer::Both,
|
tp: HttpVer::Both,
|
||||||
stream: StreamType::Tcp,
|
stream: StreamType::Tcp,
|
||||||
client_request_timeout: Duration::from_secs(5),
|
client_request_timeout: Duration::from_secs(5),
|
||||||
|
port: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,6 +441,14 @@ impl TestServerConfig {
|
||||||
self.client_request_timeout = dur;
|
self.client_request_timeout = dur;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets test server port.
|
||||||
|
///
|
||||||
|
/// By default, a random free port is determined by the OS.
|
||||||
|
pub fn port(mut self, port: u16) -> Self {
|
||||||
|
self.port = port;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A basic HTTP server controller that simplifies the process of writing integration tests for
|
/// A basic HTTP server controller that simplifies the process of writing integration tests for
|
||||||
|
@ -567,6 +578,7 @@ impl Drop for TestServer {
|
||||||
// without needing to await anything
|
// without needing to await anything
|
||||||
|
|
||||||
// signal server to stop
|
// signal server to stop
|
||||||
|
#[allow(clippy::let_underscore_future)]
|
||||||
let _ = self.server.stop(true);
|
let _ = self.server.stop(true);
|
||||||
|
|
||||||
// signal system to stop
|
// signal system to stop
|
||||||
|
|
|
@ -1,70 +1,73 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
## 4.2.0 - 2023-01-21
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
|
||||||
## 4.1.0 - 2022-03-02
|
## 4.1.0 - 2022-03-02
|
||||||
|
|
||||||
- Add support for `actix` version `0.13`. [#2675]
|
- Add support for `actix` version `0.13`. [#2675]
|
||||||
|
|
||||||
[#2675]: https://github.com/actix/actix-web/pull/2675
|
[#2675]: https://github.com/actix/actix-web/pull/2675
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0 - 2022-02-25
|
## 4.0.0 - 2022-02-25
|
||||||
|
|
||||||
- No significant changes since `4.0.0-beta.12`.
|
- No significant changes since `4.0.0-beta.12`.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.12 - 2022-02-16
|
## 4.0.0-beta.12 - 2022-02-16
|
||||||
|
|
||||||
- No significant changes since `4.0.0-beta.11`.
|
- No significant changes since `4.0.0-beta.11`.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.11 - 2022-01-31
|
## 4.0.0-beta.11 - 2022-01-31
|
||||||
|
|
||||||
- No significant changes since `4.0.0-beta.10`.
|
- No significant changes since `4.0.0-beta.10`.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.10 - 2022-01-04
|
## 4.0.0-beta.10 - 2022-01-04
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.9 - 2021-12-27
|
## 4.0.0-beta.9 - 2021-12-27
|
||||||
|
|
||||||
- No significant changes since `4.0.0-beta.8`.
|
- No significant changes since `4.0.0-beta.8`.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.8 - 2021-12-11
|
## 4.0.0-beta.8 - 2021-12-11
|
||||||
|
|
||||||
- Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920]
|
- Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920]
|
||||||
- Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920]
|
- Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
[#1920]: https://github.com/actix/actix-web/pull/1920
|
[#1920]: https://github.com/actix/actix-web/pull/1920
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.7 - 2021-09-09
|
## 4.0.0-beta.7 - 2021-09-09
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.6 - 2021-06-26
|
## 4.0.0-beta.6 - 2021-06-26
|
||||||
|
|
||||||
- Update `actix` to `0.12`. [#2277]
|
- Update `actix` to `0.12`. [#2277]
|
||||||
|
|
||||||
[#2277]: https://github.com/actix/actix-web/pull/2277
|
[#2277]: https://github.com/actix/actix-web/pull/2277
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.5 - 2021-06-17
|
## 4.0.0-beta.5 - 2021-06-17
|
||||||
- No notable changes.
|
|
||||||
|
|
||||||
|
- No notable changes.
|
||||||
|
|
||||||
## 4.0.0-beta.4 - 2021-04-02
|
## 4.0.0-beta.4 - 2021-04-02
|
||||||
- No notable changes.
|
|
||||||
|
|
||||||
|
- No notable changes.
|
||||||
|
|
||||||
## 4.0.0-beta.3 - 2021-03-09
|
## 4.0.0-beta.3 - 2021-03-09
|
||||||
- No notable changes.
|
|
||||||
|
|
||||||
|
- No notable changes.
|
||||||
|
|
||||||
## 4.0.0-beta.2 - 2021-02-10
|
## 4.0.0-beta.2 - 2021-02-10
|
||||||
|
|
||||||
- No notable changes.
|
- No notable changes.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.1 - 2021-01-07
|
## 4.0.0-beta.1 - 2021-01-07
|
||||||
|
|
||||||
- Update `pin-project` to `1.0`.
|
- Update `pin-project` to `1.0`.
|
||||||
- Update `bytes` to `1.0`. [#1813]
|
- Update `bytes` to `1.0`. [#1813]
|
||||||
- `WebsocketContext::text` now takes an `Into<bytestring::ByteString>`. [#1864]
|
- `WebsocketContext::text` now takes an `Into<bytestring::ByteString>`. [#1864]
|
||||||
|
@ -72,21 +75,21 @@
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
[#1864]: https://github.com/actix/actix-web/pull/1864
|
[#1864]: https://github.com/actix/actix-web/pull/1864
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0 - 2020-09-11
|
## 3.0.0 - 2020-09-11
|
||||||
|
|
||||||
- No significant changes from `3.0.0-beta.2`.
|
- No significant changes from `3.0.0-beta.2`.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.2 - 2020-09-10
|
## 3.0.0-beta.2 - 2020-09-10
|
||||||
|
|
||||||
- Update `actix-*` dependencies to latest versions.
|
- Update `actix-*` dependencies to latest versions.
|
||||||
|
|
||||||
|
|
||||||
## [3.0.0-beta.1] - 2020-xx-xx
|
## [3.0.0-beta.1] - 2020-xx-xx
|
||||||
|
|
||||||
- Update `actix-web` & `actix-http` dependencies to beta.1
|
- Update `actix-web` & `actix-http` dependencies to beta.1
|
||||||
- Bump minimum supported Rust version to 1.40
|
- Bump minimum supported Rust version to 1.40
|
||||||
|
|
||||||
|
|
||||||
## [3.0.0-alpha.1] - 2020-05-08
|
## [3.0.0-alpha.1] - 2020-05-08
|
||||||
|
|
||||||
- Update the actix-web dependency to 3.0.0-alpha.1
|
- Update the actix-web dependency to 3.0.0-alpha.1
|
||||||
- Update the actix dependency to 0.10.0-alpha.2
|
- Update the actix dependency to 0.10.0-alpha.2
|
||||||
- Update the actix-http dependency to 2.0.0-alpha.3
|
- Update the actix-http dependency to 2.0.0-alpha.3
|
||||||
|
@ -109,8 +112,7 @@
|
||||||
|
|
||||||
## [1.0.2] - 2019-07-20
|
## [1.0.2] - 2019-07-20
|
||||||
|
|
||||||
- Add `ws::start_with_addr()`, returning the address of the created actor, along
|
- Add `ws::start_with_addr()`, returning the address of the created actor, along with the `HttpResponse`.
|
||||||
with the `HttpResponse`.
|
|
||||||
|
|
||||||
- Add support for specifying protocols on websocket handshake #835
|
- Add support for specifying protocols on websocket handshake #835
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-web-actors"
|
name = "actix-web-actors"
|
||||||
version = "4.1.0"
|
version = "4.2.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Actix actors support for Actix Web"
|
description = "Actix actors support for Actix Web"
|
||||||
keywords = ["actix", "http", "web", "framework", "async"]
|
keywords = ["actix", "http", "web", "framework", "async"]
|
||||||
|
@ -23,7 +23,7 @@ bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
futures-core = { version = "0.3.17", default-features = false }
|
futures-core = { version = "0.3.17", default-features = false }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
tokio = { version = "1.13.1", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
> Actix actors support for Actix Web.
|
> Actix actors support for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web-actors)
|
[](https://crates.io/crates/actix-web-actors)
|
||||||
[](https://docs.rs/actix-web-actors/4.1.0)
|
[](https://docs.rs/actix-web-actors/4.2.0)
|
||||||

|

|
||||||

|

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

|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-web-codegen/4.1.0)
|
[](https://deps.rs/crate/actix-web-codegen/4.2.0)
|
||||||
[](https://crates.io/crates/actix-web-codegen)
|
[](https://crates.io/crates/actix-web-codegen)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-web-codegen)
|
- [API Documentation](https://docs.rs/actix-web-codegen)
|
||||||
- Minimum Supported Rust Version (MSRV): 1.54
|
- Minimum Supported Rust Version (MSRV): 1.59
|
||||||
|
|
||||||
## Compile Testing
|
## Compile Testing
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,7 @@ mod route;
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_web::HttpResponse;
|
/// # use actix_web::HttpResponse;
|
||||||
/// # use actix_web_codegen::route;
|
/// # use actix_web_codegen::route;
|
||||||
/// #[route("/test", method = "GET", method = "HEAD")]
|
/// #[route("/test", method = "GET", method = "HEAD", method = "CUSTOM")]
|
||||||
/// async fn example() -> HttpResponse {
|
/// async fn example() -> HttpResponse {
|
||||||
/// HttpResponse::Ok().finish()
|
/// HttpResponse::Ok().finish()
|
||||||
/// }
|
/// }
|
||||||
|
|
|
@ -6,11 +6,11 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||||
use quote::{quote, ToTokens, TokenStreamExt};
|
use quote::{quote, ToTokens, TokenStreamExt};
|
||||||
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
|
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
|
||||||
|
|
||||||
macro_rules! method_type {
|
macro_rules! standard_method_type {
|
||||||
(
|
(
|
||||||
$($variant:ident, $upper:ident, $lower:ident,)+
|
$($variant:ident, $upper:ident, $lower:ident,)+
|
||||||
) => {
|
) => {
|
||||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum MethodType {
|
pub enum MethodType {
|
||||||
$(
|
$(
|
||||||
$variant,
|
$variant,
|
||||||
|
@ -27,7 +27,7 @@ macro_rules! method_type {
|
||||||
fn parse(method: &str) -> Result<Self, String> {
|
fn parse(method: &str) -> Result<Self, String> {
|
||||||
match method {
|
match method {
|
||||||
$(stringify!($upper) => Ok(Self::$variant),)+
|
$(stringify!($upper) => Ok(Self::$variant),)+
|
||||||
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
|
_ => Err(format!("HTTP method must be uppercase: `{}`", method)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ macro_rules! method_type {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
method_type! {
|
standard_method_type! {
|
||||||
Get, GET, get,
|
Get, GET, get,
|
||||||
Post, POST, post,
|
Post, POST, post,
|
||||||
Put, PUT, put,
|
Put, PUT, put,
|
||||||
|
@ -53,13 +53,6 @@ method_type! {
|
||||||
Patch, PATCH, patch,
|
Patch, PATCH, patch,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToTokens for MethodType {
|
|
||||||
fn to_tokens(&self, stream: &mut TokenStream2) {
|
|
||||||
let ident = Ident::new(self.as_str(), Span::call_site());
|
|
||||||
stream.append(ident);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&syn::LitStr> for MethodType {
|
impl TryFrom<&syn::LitStr> for MethodType {
|
||||||
type Error = syn::Error;
|
type Error = syn::Error;
|
||||||
|
|
||||||
|
@ -69,12 +62,123 @@ impl TryFrom<&syn::LitStr> for MethodType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToTokens for MethodType {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
let ident = Ident::new(self.as_str(), Span::call_site());
|
||||||
|
stream.append(ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
enum MethodTypeExt {
|
||||||
|
Standard(MethodType),
|
||||||
|
Custom(LitStr),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MethodTypeExt {
|
||||||
|
/// Returns a single method guard token stream.
|
||||||
|
fn to_tokens_single_guard(&self) -> TokenStream2 {
|
||||||
|
match self {
|
||||||
|
MethodTypeExt::Standard(method) => {
|
||||||
|
quote! {
|
||||||
|
.guard(::actix_web::guard::#method())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MethodTypeExt::Custom(lit) => {
|
||||||
|
quote! {
|
||||||
|
.guard(::actix_web::guard::Method(
|
||||||
|
::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a multi-method guard chain token stream.
|
||||||
|
fn to_tokens_multi_guard(&self, or_chain: Vec<impl ToTokens>) -> TokenStream2 {
|
||||||
|
debug_assert!(
|
||||||
|
!or_chain.is_empty(),
|
||||||
|
"empty or_chain passed to multi-guard constructor"
|
||||||
|
);
|
||||||
|
|
||||||
|
match self {
|
||||||
|
MethodTypeExt::Standard(method) => {
|
||||||
|
quote! {
|
||||||
|
.guard(
|
||||||
|
::actix_web::guard::Any(::actix_web::guard::#method())
|
||||||
|
#(#or_chain)*
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MethodTypeExt::Custom(lit) => {
|
||||||
|
quote! {
|
||||||
|
.guard(
|
||||||
|
::actix_web::guard::Any(
|
||||||
|
::actix_web::guard::Method(
|
||||||
|
::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
#(#or_chain)*
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a token stream containing the `.or` chain to be passed in to
|
||||||
|
/// [`MethodTypeExt::to_tokens_multi_guard()`].
|
||||||
|
fn to_tokens_multi_guard_or_chain(&self) -> TokenStream2 {
|
||||||
|
match self {
|
||||||
|
MethodTypeExt::Standard(method_type) => {
|
||||||
|
quote! {
|
||||||
|
.or(::actix_web::guard::#method_type())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MethodTypeExt::Custom(lit) => {
|
||||||
|
quote! {
|
||||||
|
.or(
|
||||||
|
::actix_web::guard::Method(
|
||||||
|
::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for MethodTypeExt {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
match self {
|
||||||
|
MethodTypeExt::Custom(lit_str) => {
|
||||||
|
let ident = Ident::new(lit_str.value().as_str(), Span::call_site());
|
||||||
|
stream.append(ident);
|
||||||
|
}
|
||||||
|
MethodTypeExt::Standard(method) => method.to_tokens(stream),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&syn::LitStr> for MethodTypeExt {
|
||||||
|
type Error = syn::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
|
||||||
|
match MethodType::try_from(value) {
|
||||||
|
Ok(method) => Ok(MethodTypeExt::Standard(method)),
|
||||||
|
Err(_) if value.value().chars().all(|c| c.is_ascii_uppercase()) => {
|
||||||
|
Ok(MethodTypeExt::Custom(value.clone()))
|
||||||
|
}
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Args {
|
struct Args {
|
||||||
path: syn::LitStr,
|
path: syn::LitStr,
|
||||||
resource_name: Option<syn::LitStr>,
|
resource_name: Option<syn::LitStr>,
|
||||||
guards: Vec<Path>,
|
guards: Vec<Path>,
|
||||||
wrappers: Vec<syn::Type>,
|
wrappers: Vec<syn::Type>,
|
||||||
methods: HashSet<MethodType>,
|
methods: HashSet<MethodTypeExt>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
|
@ -99,7 +203,7 @@ impl Args {
|
||||||
|
|
||||||
let is_route_macro = method.is_none();
|
let is_route_macro = method.is_none();
|
||||||
if let Some(method) = method {
|
if let Some(method) = method {
|
||||||
methods.insert(method);
|
methods.insert(MethodTypeExt::Standard(method));
|
||||||
}
|
}
|
||||||
|
|
||||||
for arg in args {
|
for arg in args {
|
||||||
|
@ -116,6 +220,7 @@ impl Args {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
NestedMeta::Meta(syn::Meta::NameValue(nv)) => {
|
NestedMeta::Meta(syn::Meta::NameValue(nv)) => {
|
||||||
if nv.path.is_ident("name") {
|
if nv.path.is_ident("name") {
|
||||||
if let syn::Lit::Str(lit) = nv.lit {
|
if let syn::Lit::Str(lit) = nv.lit {
|
||||||
|
@ -151,11 +256,10 @@ impl Args {
|
||||||
"HTTP method forbidden here. To handle multiple methods, use `route` instead",
|
"HTTP method forbidden here. To handle multiple methods, use `route` instead",
|
||||||
));
|
));
|
||||||
} else if let syn::Lit::Str(ref lit) = nv.lit {
|
} else if let syn::Lit::Str(ref lit) = nv.lit {
|
||||||
let method = MethodType::try_from(lit)?;
|
if !methods.insert(MethodTypeExt::try_from(lit)?) {
|
||||||
if !methods.insert(method) {
|
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
&nv.lit,
|
&nv.lit,
|
||||||
&format!(
|
format!(
|
||||||
"HTTP method defined more than once: `{}`",
|
"HTTP method defined more than once: `{}`",
|
||||||
lit.value()
|
lit.value()
|
||||||
),
|
),
|
||||||
|
@ -174,11 +278,13 @@ impl Args {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
arg => {
|
arg => {
|
||||||
return Err(syn::Error::new_spanned(arg, "Unknown attribute."));
|
return Err(syn::Error::new_spanned(arg, "Unknown attribute."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Args {
|
Ok(Args {
|
||||||
path: path.unwrap(),
|
path: path.unwrap(),
|
||||||
resource_name,
|
resource_name,
|
||||||
|
@ -299,22 +405,19 @@ impl ToTokens for Route {
|
||||||
.map_or_else(|| name.to_string(), LitStr::value);
|
.map_or_else(|| name.to_string(), LitStr::value);
|
||||||
|
|
||||||
let method_guards = {
|
let method_guards = {
|
||||||
let mut others = methods.iter();
|
debug_assert!(!methods.is_empty(), "Args::methods should not be empty");
|
||||||
|
|
||||||
// unwrapping since length is checked to be at least one
|
let mut others = methods.iter();
|
||||||
let first = others.next().unwrap();
|
let first = others.next().unwrap();
|
||||||
|
|
||||||
if methods.len() > 1 {
|
if methods.len() > 1 {
|
||||||
quote! {
|
let other_method_guards = others
|
||||||
.guard(
|
.map(|method_ext| method_ext.to_tokens_multi_guard_or_chain())
|
||||||
::actix_web::guard::Any(::actix_web::guard::#first())
|
.collect();
|
||||||
#(.or(::actix_web::guard::#others()))*
|
|
||||||
)
|
first.to_tokens_multi_guard(other_method_guards)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
quote! {
|
first.to_tokens_single_guard()
|
||||||
.guard(::actix_web::guard::#first())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -325,7 +428,6 @@ impl ToTokens for Route {
|
||||||
#(.guard(::actix_web::guard::fn_guard(#guards)))*
|
#(.guard(::actix_web::guard::fn_guard(#guards)))*
|
||||||
#(.wrap(#wrappers))*
|
#(.wrap(#wrappers))*
|
||||||
.to(#name);
|
.to(#name);
|
||||||
|
|
||||||
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
|
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -86,7 +86,18 @@ async fn get_param_test(_: web::Path<String>) -> impl Responder {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[route("/multi", method = "GET", method = "POST", method = "HEAD")]
|
#[route("/hello", method = "HELLO")]
|
||||||
|
async fn custom_route_test() -> impl Responder {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[route(
|
||||||
|
"/multi",
|
||||||
|
method = "GET",
|
||||||
|
method = "POST",
|
||||||
|
method = "HEAD",
|
||||||
|
method = "HELLO"
|
||||||
|
)]
|
||||||
async fn route_test() -> impl Responder {
|
async fn route_test() -> impl Responder {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,11 @@ fn compile_macros() {
|
||||||
t.pass("tests/trybuild/route-ok.rs");
|
t.pass("tests/trybuild/route-ok.rs");
|
||||||
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
|
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
|
||||||
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
|
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
|
||||||
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
|
|
||||||
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
|
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
|
||||||
|
|
||||||
|
t.pass("tests/trybuild/route-custom-method.rs");
|
||||||
|
t.compile_fail("tests/trybuild/route-custom-lowercase.rs");
|
||||||
|
|
||||||
t.pass("tests/trybuild/routes-ok.rs");
|
t.pass("tests/trybuild/routes-ok.rs");
|
||||||
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
|
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
|
||||||
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
|
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use actix_web_codegen::*;
|
use actix_web_codegen::*;
|
||||||
|
use actix_web::http::Method;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[route("/", method="UNEXPECTED")]
|
#[route("/", method = "hello")]
|
||||||
async fn index() -> String {
|
async fn index() -> String {
|
||||||
"Hello World!".to_owned()
|
"Hello World!".to_owned()
|
||||||
}
|
}
|
||||||
|
@ -11,7 +13,7 @@ async fn main() {
|
||||||
|
|
||||||
let srv = actix_test::start(|| App::new().service(index));
|
let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
|
||||||
let request = srv.get("/");
|
let request = srv.request(Method::from_str("hello").unwrap(), srv.url("/"));
|
||||||
let response = request.send().await.unwrap();
|
let response = request.send().await.unwrap();
|
||||||
assert!(response.status().is_success());
|
assert!(response.status().is_success());
|
||||||
}
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
error: HTTP method must be uppercase: `hello`
|
||||||
|
--> tests/trybuild/route-custom-lowercase.rs:5:23
|
||||||
|
|
|
||||||
|
5 | #[route("/", method = "hello")]
|
||||||
|
| ^^^^^^^
|
||||||
|
|
||||||
|
error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied
|
||||||
|
--> tests/trybuild/route-custom-lowercase.rs:14:55
|
||||||
|
|
|
||||||
|
14 | let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future<Output = String> {index}`
|
||||||
|
| |
|
||||||
|
| required by a bound introduced by this call
|
||||||
|
|
|
||||||
|
note: required by a bound in `App::<T>::service`
|
||||||
|
--> $WORKSPACE/actix-web/src/app.rs
|
||||||
|
|
|
||||||
|
| F: HttpServiceFactory + 'static,
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ required by this bound in `App::<T>::service`
|
|
@ -0,0 +1,37 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use actix_web::http::Method;
|
||||||
|
use actix_web_codegen::route;
|
||||||
|
|
||||||
|
#[route("/single", method = "CUSTOM")]
|
||||||
|
async fn index() -> String {
|
||||||
|
"Hello Single!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[route("/multi", method = "GET", method = "CUSTOM")]
|
||||||
|
async fn custom() -> String {
|
||||||
|
"Hello Multi!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
use actix_web::App;
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| App::new().service(index).service(custom));
|
||||||
|
|
||||||
|
let request = srv.request(Method::GET, srv.url("/"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_client_error());
|
||||||
|
|
||||||
|
let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/single"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(Method::GET, srv.url("/multi"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/multi"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
error: Unexpected HTTP method: `UNEXPECTED`
|
|
||||||
--> tests/trybuild/route-unexpected-method-fail.rs:3:21
|
|
||||||
|
|
|
||||||
3 | #[route("/", method="UNEXPECTED")]
|
|
||||||
| ^^^^^^^^^^^^
|
|
||||||
|
|
||||||
error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied
|
|
||||||
--> tests/trybuild/route-unexpected-method-fail.rs:12:55
|
|
||||||
|
|
|
||||||
12 | let srv = actix_test::start(|| App::new().service(index));
|
|
||||||
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future<Output = String> {index}`
|
|
||||||
| |
|
|
||||||
| required by a bound introduced by this call
|
|
||||||
|
|
|
||||||
note: required by a bound in `App::<T>::service`
|
|
||||||
--> $WORKSPACE/actix-web/src/app.rs
|
|
||||||
|
|
|
||||||
| F: HttpServiceFactory + 'static,
|
|
||||||
| ^^^^^^^^^^^^^^^^^^ required by this bound in `App::<T>::service`
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-web"
|
name = "actix-web"
|
||||||
version = "4.2.1"
|
version = "4.3.1"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
@ -68,11 +68,11 @@ actix-service = "2"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-tls = { version = "3", default-features = false, optional = true }
|
actix-tls = { version = "3", default-features = false, optional = true }
|
||||||
|
|
||||||
actix-http = { version = "3.2.2", features = ["http2", "ws"] }
|
actix-http = { version = "3.3", features = ["http2", "ws"] }
|
||||||
actix-router = "0.5"
|
actix-router = "0.5"
|
||||||
actix-web-codegen = { version = "4.1", optional = true }
|
actix-web-codegen = { version = "4.2", optional = true }
|
||||||
|
|
||||||
ahash = "0.7"
|
ahash = "0.8"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
cfg-if = "1"
|
cfg-if = "1"
|
||||||
|
@ -93,7 +93,7 @@ serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
socket2 = "0.4.0"
|
socket2 = "0.4"
|
||||||
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ actix-test = { version = "0.1", features = ["openssl", "rustls"] }
|
||||||
awc = { version = "3", features = ["openssl"] }
|
awc = { version = "3", features = ["openssl"] }
|
||||||
|
|
||||||
brotli = "3.3.3"
|
brotli = "3.3.3"
|
||||||
const-str = "0.4"
|
const-str = "0.3"
|
||||||
criterion = { version = "0.4", features = ["html_reports"] }
|
criterion = { version = "0.4", features = ["html_reports"] }
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
flate2 = "1.0.13"
|
flate2 = "1.0.13"
|
||||||
|
@ -115,7 +115,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||||
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] }
|
||||||
zstd = "0.12"
|
zstd = "0.12"
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# 0.7.15
|
# 0.7.15
|
||||||
|
|
||||||
- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
|
- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in your routes, you should use `%20`.
|
||||||
your routes, you should use `%20`.
|
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
|
@ -29,13 +28,11 @@ fn main() {
|
||||||
|
|
||||||
# 0.7.4
|
# 0.7.4
|
||||||
|
|
||||||
- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
|
- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple even for handler with one parameter.
|
||||||
even for handler with one parameter.
|
|
||||||
|
|
||||||
# 0.7
|
# 0.7
|
||||||
|
|
||||||
- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
|
- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload use `HttpMessage::payload()` method.
|
||||||
use `HttpMessage::payload()` method.
|
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
|
@ -60,8 +57,7 @@ fn index(req: HttpRequest) -> impl Responder {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
|
- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) trait uses `&HttpRequest` instead of `&mut HttpRequest`.
|
||||||
trait uses `&HttpRequest` instead of `&mut HttpRequest`.
|
|
||||||
|
|
||||||
- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
|
- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
|
||||||
|
|
||||||
|
@ -81,14 +77,11 @@ fn index((query, json): (Query<..>, Json<MyStruct)) -> impl Responder {}
|
||||||
|
|
||||||
- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
|
- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
|
||||||
|
|
||||||
- Removed deprecated `HttpServer::threads()`, use
|
- Removed deprecated `HttpServer::threads()`, use [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
|
||||||
[HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
|
|
||||||
|
|
||||||
- Renamed `client::ClientConnectorError::Connector` to
|
- Renamed `client::ClientConnectorError::Connector` to `client::ClientConnectorError::Resolver`
|
||||||
`client::ClientConnectorError::Resolver`
|
|
||||||
|
|
||||||
- `Route::with()` does not return `ExtractorConfig`, to configure
|
- `Route::with()` does not return `ExtractorConfig`, to configure extractor use `Route::with_config()`
|
||||||
extractor use `Route::with_config()`
|
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
|
@ -116,23 +109,19 @@ fn main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `Route::with_async()` does not return `ExtractorConfig`, to configure
|
- `Route::with_async()` does not return `ExtractorConfig`, to configure extractor use `Route::with_async_config()`
|
||||||
extractor use `Route::with_async_config()`
|
|
||||||
|
|
||||||
# 0.6
|
# 0.6
|
||||||
|
|
||||||
- `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
|
- `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
|
||||||
|
|
||||||
- `ws::Message::Close` now includes optional close reason.
|
- `ws::Message::Close` now includes optional close reason. `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
|
||||||
`ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
|
|
||||||
|
|
||||||
- `HttpServer::threads()` renamed to `HttpServer::workers()`.
|
- `HttpServer::threads()` renamed to `HttpServer::workers()`.
|
||||||
|
|
||||||
- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
|
- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
|
||||||
Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
|
|
||||||
|
|
||||||
- `HttpRequest::extensions()` returns read only reference to the request's Extension
|
- `HttpRequest::extensions()` returns read only reference to the request's Extension `HttpRequest::extensions_mut()` returns mutable reference.
|
||||||
`HttpRequest::extensions_mut()` returns mutable reference.
|
|
||||||
|
|
||||||
- Instead of
|
- Instead of
|
||||||
|
|
||||||
|
@ -146,8 +135,7 @@ fn main() {
|
||||||
|
|
||||||
- `FromRequest::Result` has to implement `Into<Reply<Self>>`
|
- `FromRequest::Result` has to implement `Into<Reply<Self>>`
|
||||||
|
|
||||||
- [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
|
- [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) is generic over `S`
|
||||||
is generic over `S`
|
|
||||||
|
|
||||||
- Use `Query` extractor instead of HttpRequest::query()`.
|
- Use `Query` extractor instead of HttpRequest::query()`.
|
||||||
|
|
||||||
|
@ -163,23 +151,19 @@ or
|
||||||
let q = Query::<HashMap<String, String>>::extract(req);
|
let q = Query::<HashMap<String, String>>::extract(req);
|
||||||
```
|
```
|
||||||
|
|
||||||
- Websocket operations are implemented as `WsWriter` trait.
|
- Websocket operations are implemented as `WsWriter` trait. you need to use `use actix_web::ws::WsWriter`
|
||||||
you need to use `use actix_web::ws::WsWriter`
|
|
||||||
|
|
||||||
# 0.5
|
# 0.5
|
||||||
|
|
||||||
- `HttpResponseBuilder::body()`, `.finish()`, `.json()`
|
- `HttpResponseBuilder::body()`, `.finish()`, `.json()` methods return `HttpResponse` instead of `Result<HttpResponse>`
|
||||||
methods return `HttpResponse` instead of `Result<HttpResponse>`
|
|
||||||
|
|
||||||
- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
|
- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` moved to `actix_web::http` module
|
||||||
moved to `actix_web::http` module
|
|
||||||
|
|
||||||
- `actix_web::header` moved to `actix_web::http::header`
|
- `actix_web::header` moved to `actix_web::http::header`
|
||||||
|
|
||||||
- `NormalizePath` moved to `actix_web::http` module
|
- `NormalizePath` moved to `actix_web::http` module
|
||||||
|
|
||||||
- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
|
- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, shortcut for `actix_web::server::HttpServer::new()`
|
||||||
shortcut for `actix_web::server::HttpServer::new()`
|
|
||||||
|
|
||||||
- `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
|
- `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
|
||||||
|
|
||||||
|
@ -187,11 +171,9 @@ let q = Query::<HashMap<String, String>>::extract(req);
|
||||||
|
|
||||||
- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
|
- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
|
||||||
|
|
||||||
- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
|
- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` functions should be used instead
|
||||||
functions should be used instead
|
|
||||||
|
|
||||||
- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
|
- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` instead of `Result<_, http::Error>`
|
||||||
instead of `Result<_, http::Error>`
|
|
||||||
|
|
||||||
- `Application` renamed to a `App`
|
- `Application` renamed to a `App`
|
||||||
|
|
||||||
|
|
|
@ -88,8 +88,7 @@
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
- Resource registration. 1.0 version uses generalized resource
|
- Resource registration. 1.0 version uses generalized resource registration via `.service()` method.
|
||||||
registration via `.service()` method.
|
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
|
@ -97,9 +96,7 @@
|
||||||
App.new().resource("/welcome", |r| r.f(welcome))
|
App.new().resource("/welcome", |r| r.f(welcome))
|
||||||
```
|
```
|
||||||
|
|
||||||
use App's or Scope's `.service()` method. `.service()` method accepts
|
use App's or Scope's `.service()` method. `.service()` method accepts object that implements `HttpServiceFactory` trait. By default actix-web provides `Resource` and `Scope` services.
|
||||||
object that implements `HttpServiceFactory` trait. By default
|
|
||||||
actix-web provides `Resource` and `Scope` services.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
App.new().service(
|
App.new().service(
|
||||||
|
@ -164,9 +161,7 @@
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `.f()`, `.a()` and `.h()` handler registration methods have been removed.
|
- `.f()`, `.a()` and `.h()` handler registration methods have been removed. Use `.to()` for handlers and `.to_async()` for async handlers. Handler function must use extractors.
|
||||||
Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
|
|
||||||
must use extractors.
|
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
|
@ -210,9 +205,7 @@
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `State` is now `Data`. You register Data during the App initialization process
|
- `State` is now `Data`. You register Data during the App initialization process and then access it from handlers either using a Data extractor or using HttpRequest's api.
|
||||||
and then access it from handlers either using a Data extractor or using
|
|
||||||
HttpRequest's api.
|
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
|
@ -277,8 +270,7 @@
|
||||||
.route("/index.html", web::get().to(index));
|
.route("/index.html", web::get().to(index));
|
||||||
```
|
```
|
||||||
|
|
||||||
- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
|
- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
|
||||||
method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
|
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
|
@ -317,8 +309,7 @@
|
||||||
|
|
||||||
use `use actix_multipart::Multipart`
|
use `use actix_multipart::Multipart`
|
||||||
|
|
||||||
- Response compression is not enabled by default.
|
- Response compression is not enabled by default. To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
|
||||||
To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
|
|
||||||
|
|
||||||
- Session middleware moved to actix-session crate
|
- Session middleware moved to actix-session crate
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,16 @@
|
||||||
# Migrating to 2.0.0
|
# Migrating to 2.0.0
|
||||||
|
|
||||||
- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
|
- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to `.await` on `run` method result, in that case it awaits server exit.
|
||||||
`.await` on `run` method result, in that case it awaits server exit.
|
|
||||||
|
|
||||||
- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`.
|
- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`. Stored data is available via `HttpRequest::app_data()` method at runtime.
|
||||||
Stored data is available via `HttpRequest::app_data()` method at runtime.
|
|
||||||
|
|
||||||
- Extractor configuration must be registered with `App::app_data()` instead of `App::data()`
|
- Extractor configuration must be registered with `App::app_data()` instead of `App::data()`
|
||||||
|
|
||||||
- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()`
|
- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()` replace `fn` with `async fn` to convert sync handler to async
|
||||||
replace `fn` with `async fn` to convert sync handler to async
|
|
||||||
|
|
||||||
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start
|
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start test server use `test::start()` or `test_start_with_config()` methods
|
||||||
test server use `test::start()` or `test_start_with_config()` methods
|
|
||||||
|
|
||||||
- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders
|
- `ResponseError` trait has been refactored. `ResponseError::error_response()` renders http response.
|
||||||
http response.
|
|
||||||
|
|
||||||
- Feature `rust-tls` renamed to `rustls`
|
- Feature `rust-tls` renamed to `rustls`
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,23 @@
|
||||||
# Migrating to 3.0.0
|
# Migrating to 3.0.0
|
||||||
|
|
||||||
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
|
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
|
||||||
simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
|
|
||||||
|
|
||||||
- Cookie handling has been offloaded to the `cookie` crate:
|
- Cookie handling has been offloaded to the `cookie` crate:
|
||||||
|
|
||||||
- `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
- `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
||||||
- Some types now require lifetime parameters.
|
- Some types now require lifetime parameters.
|
||||||
|
|
||||||
- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
|
- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects any `actix-web` method previously expecting a time v0.1 input.
|
||||||
any `actix-web` method previously expecting a time v0.1 input.
|
|
||||||
|
|
||||||
- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now result in `SameSite=None` being sent with the response Set-Cookie header. To create a cookie without a SameSite attribute, remove any calls setting same_site.
|
||||||
result in `SameSite=None` being sent with the response Set-Cookie header.
|
|
||||||
To create a cookie without a SameSite attribute, remove any calls setting same_site.
|
|
||||||
|
|
||||||
- actix-http support for Actors messages was moved to actix-http crate and is enabled
|
- actix-http support for Actors messages was moved to actix-http crate and is enabled with feature `actors`
|
||||||
with feature `actors`
|
|
||||||
|
|
||||||
- content_length function is removed from actix-http.
|
- content_length function is removed from actix-http. You can set Content-Length by normally setting the response body or calling no_chunking function.
|
||||||
You can set Content-Length by normally setting the response body or calling no_chunking function.
|
|
||||||
|
|
||||||
- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`.
|
||||||
`u64` instead of a `usize`.
|
|
||||||
|
|
||||||
- Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use
|
- Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use destructuring or `.into_inner()`. For example:
|
||||||
destructuring or `.into_inner()`. For example:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Previously:
|
// Previously:
|
||||||
|
@ -44,9 +36,7 @@
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
|
- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
|
||||||
It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`,
|
|
||||||
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
|
|
||||||
|
|
||||||
- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.
|
- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ Headings marked with :warning: are **breaking behavioral changes**. They will pr
|
||||||
- [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously)
|
- [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously)
|
||||||
- [`#[actix_web::main]` and `#[tokio::main]`](#actix_webmain-and-tokiomain)
|
- [`#[actix_web::main]` and `#[tokio::main]`](#actix_webmain-and-tokiomain)
|
||||||
- [`web::block`](#webblock)
|
- [`web::block`](#webblock)
|
||||||
-
|
-
|
||||||
|
|
||||||
## MSRV
|
## MSRV
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,7 @@
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web)
|
[](https://crates.io/crates/actix-web) [](https://docs.rs/actix-web/4.3.1)   [](https://deps.rs/crate/actix-web/4.3.1) <br /> [](https://github.com/actix/actix-web/actions/workflows/ci.yml) [](https://codecov.io/gh/actix/actix-web)  [](https://discord.gg/NWpN5mmg3x)
|
||||||
[](https://docs.rs/actix-web/4.2.1)
|
|
||||||

|
|
||||||

|
|
||||||
[](https://deps.rs/crate/actix-web/4.2.1)
|
|
||||||
<br />
|
|
||||||
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
|
||||||
[](https://codecov.io/gh/actix/actix-web)
|
|
||||||

|
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use actix_web::{web, App, HttpResponse};
|
use actix_web::{web, App, HttpResponse};
|
||||||
use awc::Client;
|
use awc::Client;
|
||||||
use criterion::{criterion_group, criterion_main, Criterion};
|
use criterion::{criterion_group, criterion_main, Criterion};
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer};
|
use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer};
|
||||||
|
|
||||||
#[get("/resource1/{name}/index.html")]
|
#[get("/resource1/{name}/index.html")]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer};
|
use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer};
|
||||||
|
|
||||||
async fn index(req: HttpRequest) -> &'static str {
|
async fn index(req: HttpRequest) -> &'static str {
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
//! For an example of extracting a client TLS certificate, see:
|
//! For an example of extracting a client TLS certificate, see:
|
||||||
//! <https://github.com/actix/examples/tree/master/https-tls/rustls-client-cert>
|
//! <https://github.com/actix/examples/tree/master/https-tls/rustls-client-cert>
|
||||||
|
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use std::{any::Any, io, net::SocketAddr};
|
use std::{any::Any, io, net::SocketAddr};
|
||||||
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
use actix_web::{get, web, HttpRequest};
|
use actix_web::{get, web, HttpRequest};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use actix_web::{middleware, App, Error, HttpResponse, HttpServer};
|
use actix_web::{middleware, App, Error, HttpResponse, HttpServer};
|
||||||
|
@ -39,7 +41,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
)
|
)
|
||||||
.service(web::resource("/test1.html").to(|| async { "Test\r\n" }))
|
.service(web::resource("/test1.html").to(|| async { "Test\r\n" }))
|
||||||
})
|
})
|
||||||
.bind_uds("/Users/fafhrd91/uds-test")?
|
.bind_uds("/Users/me/uds-test")?
|
||||||
.workers(1)
|
.workers(1)
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -712,6 +712,7 @@ mod tests {
|
||||||
.route("/", web::to(|| async { "hello" }))
|
.route("/", web::to(|| async { "hello" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::let_underscore_future)]
|
||||||
let _ = init_service(my_app());
|
let _ = init_service(my_app());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue