diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fa06a137a..a26842245 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,34 +3,40 @@ name: Bug Report about: Create a bug report. --- -Your issue may already be reported! -Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one. +Your issue may already be reported! Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one. ## Expected Behavior + ## Current Behavior + ## Possible Solution + ## Steps to Reproduce (for bugs) + + 1. 2. 3. 4. ## Context + ## Your Environment + - Rust Version (I.e, output of `rustc -V`): diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d617cf708..e0d17fb27 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,12 +2,14 @@ ## PR Type + + PR_TYPE - ## PR Checklist + @@ -17,11 +19,10 @@ PR_TYPE - [ ] Format code with the latest stable rustfmt. - [ ] (Team) Label with affected crates and semver status. - ## Overview + - diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 677ba8ef2..000000000 --- a/.prettierrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "proseWrap": "never" -} diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 000000000..7b5590248 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1 @@ +proseWrap: never diff --git a/Cargo.toml b/Cargo.toml index 26b5b91b2..65e3c6ae8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "actix-http-test", "actix-http", "actix-multipart", + "actix-multipart-derive", "actix-router", "actix-test", "actix-web-actors", @@ -27,6 +28,7 @@ actix-files = { path = "actix-files" } actix-http = { path = "actix-http" } actix-http-test = { path = "actix-http-test" } actix-multipart = { path = "actix-multipart" } +actix-multipart-derive = { path = "actix-multipart-derive" } actix-router = { path = "actix-router" } actix-test = { path = "actix-test" } actix-web = { path = "actix-web" } diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 6e57bf7a7..b4b2fd8c1 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,19 +1,24 @@ # Changes ## Unreleased - 2022-xx-xx + +## 0.6.3 - 2023-01-21 + - XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. +- Update `tokio-uring` dependency to `0.4`. [#2903]: https://github.com/actix/actix-web/pull/2903 ## 0.6.2 - 2022-07-23 + - Allow partial range responses for video content to start streaming sooner. [#2817] - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. [#2817]: https://github.com/actix/actix-web/pull/2817 - ## 0.6.1 - 2022-06-11 + - Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021] - Update `tokio-uring` dependency to `0.3`. - Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645] @@ -22,46 +27,46 @@ [#2021]: https://github.com/actix/actix-web/pull/2021 [#2645]: https://github.com/actix/actix-web/pull/2645 - ## 0.6.0 - 2022-02-25 + - No significant changes since `0.6.0-beta.16`. - ## 0.6.0-beta.16 - 2022-01-31 + - No significant changes since `0.6.0-beta.15`. - ## 0.6.0-beta.15 - 2022-01-21 + - No significant changes since `0.6.0-beta.14`. - ## 0.6.0-beta.14 - 2022-01-14 + - The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583] [#2583]: https://github.com/actix/actix-web/pull/2583 - ## 0.6.0-beta.13 - 2022-01-04 + - The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398] - The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398] - Minimum supported Rust version (MSRV) is now 1.54. [#2398]: https://github.com/actix/actix-web/pull/2398 - ## 0.6.0-beta.12 - 2021-12-29 + - No significant changes since `0.6.0-beta.11`. - ## 0.6.0-beta.11 - 2021-12-27 + - No significant changes since `0.6.0-beta.10`. - ## 0.6.0-beta.10 - 2021-12-11 + - No significant changes since `0.6.0-beta.9`. - ## 0.6.0-beta.9 - 2021-11-22 + - Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408] - Add `NamedFile::open_async`. [#2408] - Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] @@ -72,24 +77,24 @@ [#2408]: https://github.com/actix/actix-web/pull/2408 [#2453]: https://github.com/actix/actix-web/pull/2453 - ## 0.6.0-beta.8 - 2021-10-20 + - Minimum supported Rust version (MSRV) is now 1.52. - ## 0.6.0-beta.7 - 2021-09-09 + - Minimum supported Rust version (MSRV) is now 1.51. - ## 0.6.0-beta.6 - 2021-06-26 + - Added `Files::path_filter()`. [#2274] - `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] [#2274]: https://github.com/actix/actix-web/pull/2274 [#2228]: https://github.com/actix/actix-web/pull/2228 - ## 0.6.0-beta.5 - 2021-06-17 + - `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] - For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] - `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] @@ -100,58 +105,58 @@ [#2225]: https://github.com/actix/actix-web/pull/2225 [#2257]: https://github.com/actix/actix-web/pull/2257 - ## 0.6.0-beta.4 - 2021-04-02 + - Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046] [#2046]: https://github.com/actix/actix-web/pull/2046 - ## 0.6.0-beta.3 - 2021-03-09 + - No notable changes. - ## 0.6.0-beta.2 - 2021-02-10 + - Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] - Replace `v_htmlescape` with `askama_escape`. [#1953] [#1887]: https://github.com/actix/actix-web/pull/1887 [#1953]: https://github.com/actix/actix-web/pull/1953 - ## 0.6.0-beta.1 - 2021-01-07 + - `HttpRange::parse` now has its own error type. - Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 - ## 0.5.0 - 2020-12-26 + - Optionally support hidden files/directories. [#1811] [#1811]: https://github.com/actix/actix-web/pull/1811 - ## 0.4.1 - 2020-11-24 + - Clarify order of parameters in `Files::new` and improve docs. - ## 0.4.0 - 2020-10-06 + - Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714] [#1714]: https://github.com/actix/actix-web/pull/1714 - ## 0.3.0 - 2020-09-11 + - No significant changes from 0.3.0-beta.1. - ## 0.3.0-beta.1 - 2020-07-15 + - Update `v_htmlescape` to 0.10 - Update `actix-web` and `actix-http` dependencies to beta.1 - ## 0.3.0-alpha.1 - 2020-05-23 + - Update `actix-web` and `actix-http` dependencies to alpha - Fix some typos in the docs - Bump minimum supported Rust version to 1.40 @@ -159,73 +164,73 @@ [#1384]: https://github.com/actix/actix-web/pull/1384 - ## 0.2.1 - 2019-12-22 + - Use the same format for file URLs regardless of platforms - ## 0.2.0 - 2019-12-20 + - Fix BodyEncoding trait import #1220 - ## 0.2.0-alpha.1 - 2019-12-07 + - Migrate to `std::future` - ## 0.1.7 - 2019-11-06 -- Add an additional `filename*` param in the `Content-Disposition` header of - `actix_files::NamedFile` to be more compatible. (#1151) + +- Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) ## 0.1.6 - 2019-10-14 + - Add option to redirect to a slash-ended path `Files` #1132 - ## 0.1.5 - 2019-10-08 + - Bump up `mime_guess` crate version to 2.0.1 - Bump up `percent-encoding` crate version to 2.1 - Allow user defined request guards for `Files` #1113 - ## 0.1.4 - 2019-07-20 + - Allow to disable `Content-Disposition` header #686 - ## 0.1.3 - 2019-06-28 + - Do not set `Content-Length` header, let actix-http set it #930 - ## 0.1.2 - 2019-06-13 + - Content-Length is 0 for NamedFile HEAD request #914 - Fix ring dependency from actix-web default features for #741 - ## 0.1.1 - 2019-06-01 + - Static files are incorrectly served as both chunked and with length #812 - ## 0.1.0 - 2019-05-25 + - NamedFile last-modified check always fails due to nano-seconds in file modified date #820 - ## 0.1.0-beta.4 - 2019-05-12 + - Update actix-web to beta.4 - ## 0.1.0-beta.1 - 2019-04-20 + - Update actix-web to beta.1 - ## 0.1.0-alpha.6 - 2019-04-14 + - Update actix-web to alpha6 - ## 0.1.0-alpha.4 - 2019-04-08 + - Update actix-web to alpha4 - ## 0.1.0-alpha.2 - 2019-04-02 + - Add default handler support - ## 0.1.0-alpha.1 - 2019-03-28 + - Initial impl diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 01dc2928a..4c29c95b2 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.2" +version = "0.6.3" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -40,8 +40,8 @@ v_htmlescape= "0.15" # experimental-io-uring [target.'cfg(target_os = "linux")'.dependencies] -tokio-uring = { version = "0.3", optional = true, features = ["bytes"] } -actix-server = { version = "2.1", optional = true } # ensure matching tokio-uring versions +tokio-uring = { version = "0.4", optional = true, features = ["bytes"] } +actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions [dev-dependencies] actix-rt = "2.7" diff --git a/actix-files/README.md b/actix-files/README.md index a5078c8d5..8869ca436 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.2)](https://docs.rs/actix-files/0.6.2) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.3)](https://docs.rs/actix-files/0.6.3) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.2/status.svg)](https://deps.rs/crate/actix-files/0.6.2) +[![dependency status](https://deps.rs/crate/actix-files/0.6.3/status.svg)](https://deps.rs/crate/actix-files/0.6.3) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) @@ -15,4 +15,4 @@ - [API Documentation](https://docs.rs/actix-files) - [Example Project](https://github.com/actix/examples/tree/master/basics/static-files) -- Minimum Supported Rust Version (MSRV): 1.54 +- Minimum Supported Rust Version (MSRV): 1.59 diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index a30ce6fd3..be2a450d2 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -142,7 +142,7 @@ impl Files { self } - /// Set custom directory renderer + /// Set custom directory renderer. pub fn files_listing_renderer(mut self, f: F) -> Self where for<'r, 's> F: @@ -152,7 +152,7 @@ impl Files { self } - /// Specifies mime override callback + /// Specifies MIME override callback. pub fn mime_override(mut self, f: F) -> Self where F: Fn(&mime::Name<'_>) -> DispositionType + 'static, @@ -390,3 +390,42 @@ impl ServiceFactory 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 + ); + } +} diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 9ee1338c6..650f55247 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -30,7 +30,7 @@ impl PathBufWrap { let mut segment_count = path.matches('/').count() + 1; // we can decode the whole path here (instead of per-segment decoding) - // because we will reject `%2F` in paths using `segement_count`. + // because we will reject `%2F` in paths using `segment_count`. let path = percent_encoding::percent_decode_str(path) .decode_utf8() .map_err(|_| UriSegmentError::NotValidUtf8)?; diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 028fe3ddc..e56883c19 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,10 +1,13 @@ # Changes ## Unreleased - 2022-xx-xx + +## 3.1.0 - 2023-01-21 + - Minimum supported Rust version (MSRV) is now 1.59. - ## 3.0.0 - 2022-07-24 + - `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] - Added `TestServer::client_headers` method. [#2097] - Update `actix-server` dependency to `2`. @@ -16,71 +19,71 @@ [#2097]: https://github.com/actix/actix-web/pull/2097 [#1813]: https://github.com/actix/actix-web/pull/1813 -
3.0.0 Pre-Releases ## 3.0.0-beta.13 - 2022-02-16 + - No significant changes since `3.0.0-beta.12`. - ## 3.0.0-beta.12 - 2022-01-31 + - No significant changes since `3.0.0-beta.11`. - ## 3.0.0-beta.11 - 2022-01-04 + - Minimum supported Rust version (MSRV) is now 1.54. - ## 3.0.0-beta.10 - 2021-12-27 + - Update `actix-server` to `2.0.0-rc.2`. [#2550] [#2550]: https://github.com/actix/actix-web/pull/2550 - ## 3.0.0-beta.9 - 2021-12-11 + - No significant changes since `3.0.0-beta.8`. - ## 3.0.0-beta.8 - 2021-11-30 + - Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2474]: https://github.com/actix/actix-web/pull/2474 - ## 3.0.0-beta.7 - 2021-11-22 + - Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 - ## 3.0.0-beta.6 - 2021-11-15 + - `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] - Update `actix-server` to `2.0.0-beta.9`. [#2442] - Minimum supported Rust version (MSRV) is now 1.52. [#2442]: https://github.com/actix/actix-web/pull/2442 - ## 3.0.0-beta.5 - 2021-09-09 + - Minimum supported Rust version (MSRV) is now 1.51. - ## 3.0.0-beta.4 - 2021-04-02 + - Added `TestServer::client_headers` method. [#2097] [#2097]: https://github.com/actix/actix-web/pull/2097 - ## 3.0.0-beta.3 - 2021-03-09 -- No notable changes. +- No notable changes. ## 3.0.0-beta.2 - 2021-02-10 + - No notable changes. - ## 3.0.0-beta.1 - 2021-01-07 + - Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 @@ -88,6 +91,7 @@
## 2.1.0 - 2020-11-25 + - Add ability to set address for `TestServer`. [#1645] - Upgrade `base64` to `0.13`. - Upgrade `serde_urlencoded` to `0.7`. [#1773] @@ -95,12 +99,12 @@ [#1773]: https://github.com/actix/actix-web/pull/1773 [#1645]: https://github.com/actix/actix-web/pull/1645 - ## 2.0.0 - 2020-09-11 + - Update actix-codec and actix-utils dependencies. - ## 2.0.0-alpha.1 - 2020-05-23 + - Update the `time` dependency to 0.2.7 - Update `actix-connect` dependency to 2.0.0-alpha.2 - Make `test_server` `async` fn. @@ -110,55 +114,56 @@ - Update `env_logger` dependency to 0.7 ## 1.0.0 - 2019-12-13 + - Replaced `TestServer::start()` with `test_server()` - ## 1.0.0-alpha.3 - 2019-12-07 + - Migrate to `std::future` - ## 0.2.5 - 2019-09-17 + - Update serde_urlencoded to "0.6.1" - Increase TestServerRuntime timeouts from 500ms to 3000ms - Do not override current `System` - ## 0.2.4 - 2019-07-18 + - Update actix-server to 0.6 - ## 0.2.3 - 2019-07-16 + - Add `delete`, `options`, `patch` methods to `TestServerRunner` - ## 0.2.2 - 2019-06-16 + - Add .put() and .sput() methods - ## 0.2.1 - 2019-06-05 + - Add license files - ## 0.2.0 - 2019-05-12 + - Update awc and actix-http deps - ## 0.1.1 - 2019-04-24 + - Always make new connection for http client - ## 0.1.0 - 2019-04-16 + - No changes - ## 0.1.0-alpha.3 - 2019-04-02 + - Request functions accept path #743 - ## 0.1.0-alpha.2 - 2019-03-29 + - Added TestServerRuntime::load_body() method - Update actix-http and awc libraries - ## 0.1.0-alpha.1 - 2019-03-28 + - Initial impl diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 86338fb06..cd5e87162 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0" +version = "3.1.0" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] @@ -37,7 +37,6 @@ actix-rt = "2.2" actix-server = "2" awc = { version = "3", default-features = false } -base64 = "0.13" bytes = "1" futures-core = { version = "0.3.17", default-features = false } http = "0.2.5" @@ -48,7 +47,7 @@ serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" 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] actix-web = { version = "4", default-features = false, features = ["cookies"] } diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 25e7c684e..94f0e88a5 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,15 +3,15 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0)](https://docs.rs/actix-http-test/3.0.0) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.1.0)](https://docs.rs/actix-http-test/3.1.0) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.1.0/status.svg)](https://deps.rs/crate/actix-http-test/3.1.0) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http-test) -- Minimum Supported Rust Version (MSRV): 1.54 +- Minimum Supported Rust Version (MSRV): 1.59 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 7feec2a1a..033cd994c 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,13 +1,19 @@ # Changes ## Unreleased - 2022-xx-xx + +## 3.3.0 - 2023-01-21 + ### Added + - Implement `MessageBody` for `Cow<'static, str>` and `Cow<'static, [u8]>`. [#2959] - Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868] - Implement `MessageBody` for `Pin` where `B::Target: MessageBody`. [#2868] - Automatic h2c detection via new service finalizer `HttpService::tcp_auto_h2c()`. [#2957] -- `HeaderMap::retain()` [#2955]. -- Header name constants in `header` module. [#2956] +- `HeaderMap::retain()`. [#2955] +- Header name constants in `header` module. [#2956] [#2968] + - `CACHE_STATUS` + - `CDN_CACHE_CONTROL` - `CROSS_ORIGIN_EMBEDDER_POLICY` - `CROSS_ORIGIN_OPENER_POLICY` - `PERMISSIONS_POLICY` @@ -15,84 +21,103 @@ - `X_FORWARDED_HOST` - `X_FORWARDED_PROTO` +### Fixed + +- Fix non-empty body of HTTP/2 HEAD responses. [#2920] + ### Performance + - Improve overall performance of operations on `Extensions`. [#2890] [#2959]: https://github.com/actix/actix-web/pull/2959 [#2868]: https://github.com/actix/actix-web/pull/2868 [#2890]: https://github.com/actix/actix-web/pull/2890 +[#2920]: https://github.com/actix/actix-web/pull/2920 [#2957]: https://github.com/actix/actix-web/pull/2957 [#2955]: https://github.com/actix/actix-web/pull/2955 [#2956]: https://github.com/actix/actix-web/pull/2956 - +[#2968]: https://github.com/actix/actix-web/pull/2968 ## 3.2.2 - 2022-09-11 + ### Changed + - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. ### Fixed + - Avoid possibility of dispatcher getting stuck while back-pressuring I/O. [#2369] [#2369]: https://github.com/actix/actix-web/pull/2369 - ## 3.2.1 - 2022-07-02 + ### Fixed + - Fix parsing ambiguity in Transfer-Encoding and Content-Length headers for HTTP/1.0 requests. [#2794] [#2794]: https://github.com/actix/actix-web/pull/2794 - ## 3.2.0 - 2022-06-30 + ### Changed + - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. ### Fixed + - Websocket parser no longer throws endless overflow errors after receiving an oversized frame. [#2790] - Retain previously set Vary headers when using compression encoder. [#2798] [#2790]: https://github.com/actix/actix-web/pull/2790 [#2798]: https://github.com/actix/actix-web/pull/2798 - ## 3.1.0 - 2022-06-11 + ### Changed + - Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. ### Fixed + - Revert broken fix in [#2624] that caused erroneous 500 error responses. Temporarily re-introduces [#2357] bug. [#2779] [#2624]: https://github.com/actix/actix-web/pull/2624 [#2357]: https://github.com/actix/actix-web/issues/2357 [#2779]: https://github.com/actix/actix-web/pull/2779 - ## 3.0.4 - 2022-03-09 + ### Fixed + - Document on docs.rs with `ws` feature enabled. - ## 3.0.3 - 2022-03-08 + ### Fixed + - Allow spaces between header name and colon when parsing responses. [#2684] [#2684]: https://github.com/actix/actix-web/pull/2684 - ## 3.0.2 - 2022-03-05 + ### Fixed + - Fix encoding camel-case header names with more than one hyphen. [#2683] [#2683]: https://github.com/actix/actix-web/pull/2683 - ## 3.0.1 - 2022-03-04 + - Fix panic in H1 dispatcher when pipelining is used with keep-alive. [#2678] [#2678]: https://github.com/actix/actix-web/issues/2678 ## 3.0.0 - 2022-02-25 + ### Dependencies + - Updated `actix-*` to Tokio v1-based versions. [#1813] - Updated `bytes` to `1.0`. [#1813] - Updated `h2` to `0.3`. [#1813] @@ -101,6 +126,7 @@ - Updated `tokio` to `1`. ### Added + - Crate Features: - `ws`; disabled by default. [#2618] - `http2`; disabled by default. [#2618] @@ -169,6 +195,7 @@ - `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920] ### Changed + - Traits: - Rename `IntoHeaderValue => TryIntoHeaderValue`. [#2510] - `MessageBody` now has an associated `Error` type. [#2183] @@ -224,6 +251,7 @@ - Minimum supported Rust version (MSRV) is now 1.54. ### Fixed + - A `Vary` header is now correctly sent along with compressed content. [#2501] - HTTP/1.1 dispatcher correctly uses client request timeout. [#2611] - Fixed issue where handlers that took payload but then dropped without reading it to EOF it would cause keep-alive connections to become stuck. [#2624] @@ -236,6 +264,7 @@ - Fixed quality parse error in Accept-Encoding header. [#2344] ### Removed + - Crate Features: - `compress` feature. [#2065] - `cookies` feature. [#2065] @@ -298,7 +327,6 @@ - `downcast` and `downcast_get_type_id` macros. [#2291] - Down-casting for `MessageBody` types; use standard `Any` trait. [#2183] - [#1813]: https://github.com/actix/actix-web/pull/1813 [#1845]: https://github.com/actix/actix-web/pull/1845 [#1857]: https://github.com/actix/actix-web/pull/1857 @@ -371,37 +399,42 @@ [#2660]: https://github.com/actix/actix-web/pull/2660 [00ba8d55]: https://github.com/actix/actix-web/commit/00ba8d55492284581695d824648590715a8bd386 -
3.0.0 Pre-Releases ## 3.0.0-rc.4 - 2022-02-22 + ### Fixed + - Fix h1 dispatcher panic. [1ce58ecb] [1ce58ecb]: https://github.com/actix/actix-web/commit/1ce58ecb305c60e51db06e6c913b7a1344e229ca - ## 3.0.0-rc.3 - 2022-02-16 + - No significant changes since `3.0.0-rc.2`. - ## 3.0.0-rc.2 - 2022-02-08 + ### Added + - Implement `From>` for `Response>`. [#2625] ### Changed + - `error::DispatcherError` enum is now marked `#[non_exhaustive]`. [#2624] ### Fixed + - Issue where handlers that took payload but then dropped without reading it to EOF it would cause keep-alive connections to become stuck. [#2624] [#2624]: https://github.com/actix/actix-web/pull/2624 [#2625]: https://github.com/actix/actix-web/pull/2625 - ## 3.0.0-rc.1 - 2022-01-31 + ### Added + - Implement `Default` for `KeepAlive`. [#2611] - Implement `From` for `KeepAlive`. [#2611] - Implement `From>` for `KeepAlive`. [#2611] @@ -410,6 +443,7 @@ - Crate `http2` feature flag, disabled by default. [#2618] ### Changed + - Rename `ServiceConfig::{client_timer_expire => client_request_deadline}`. [#2611] - Rename `ServiceConfig::{client_disconnect_timer => client_disconnect_deadline}`. [#2611] - Deadline methods in `ServiceConfig` now return `std::time::Instant`s instead of Tokio's wrapper type. [#2611] @@ -420,9 +454,11 @@ - `ServiceConfig::keep_alive` now returns a `KeepAlive`. [#2611] ### Fixed + - HTTP/1.1 dispatcher correctly uses client request timeout. [#2611] ### Removed + - `ServiceConfig::{client_timer, keep_alive_timer}`. [#2611] - `impl From for KeepAlive`; use `Duration`s instead. [#2611] - `impl From> for KeepAlive`; use `Duration`s instead. [#2611] @@ -431,16 +467,19 @@ [#2611]: https://github.com/actix/actix-web/pull/2611 [#2618]: https://github.com/actix/actix-web/pull/2618 - ## 3.0.0-beta.19 - 2022-01-21 + ### Added + - Response headers can be sent as camel case using `res.head_mut().set_camel_case_headers(true)`. [#2587] - `ResponseHead` now implements `Clone`. [#2585] ### Changed + - Brotli (de)compression support is now provided by the `brotli` crate. [#2538] ### Removed + - `ResponseHead::extensions[_mut]()`. [#2585] - `ResponseBuilder::extensions[_mut]()`. [#2585] @@ -448,9 +487,10 @@ [#2585]: https://github.com/actix/actix-web/pull/2585 [#2587]: https://github.com/actix/actix-web/pull/2587 - ## 3.0.0-beta.18 - 2022-01-04 + ### Added + - `impl Eq` for `header::ContentEncoding`. [#2501] - `impl Copy` for `QualityItem` where `T: Copy`. [#2501] - `Quality::ZERO` equivalent to `q=0`. [#2501] @@ -458,6 +498,7 @@ - `ContentEncoding::to_header_value()`. [#2501] ### Changed + - `Quality::MIN` is now the smallest non-zero value. [#2501] - `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501] - Rename `ContentEncoding::{Br => Brotli}`. [#2501] @@ -465,19 +506,22 @@ - Minimum supported Rust version (MSRV) is now 1.54. ### Fixed + - `ContentEncoding::Identity` can now be parsed from a string. [#2501] - A `Vary` header is now correctly sent along with compressed content. [#2501] ### Removed + - `ContentEncoding::Auto` variant. [#2501] - `ContentEncoding::is_compression()`. [#2501] [#2501]: https://github.com/actix/actix-web/pull/2501 [#2565]: https://github.com/actix/actix-web/pull/2565 - ## 3.0.0-beta.17 - 2021-12-27 + ### Changed + - `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527] - `Payload` inner fields are now named. [#2545] - `impl Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545] @@ -486,30 +530,35 @@ - Rename `PayloadStream` to `BoxedPayloadStream`. [#2545] ### Removed + - `h1::Payload::readany`. [#2545] [#2527]: https://github.com/actix/actix-web/pull/2527 [#2545]: https://github.com/actix/actix-web/pull/2545 - ## 3.0.0-beta.16 - 2021-12-17 + ### Added + - New method on `MessageBody` trait, `try_into_bytes`, with default implementation, for optimizations on body types that complete in exactly one poll. Replaces `is_complete_body` and `take_complete_body`. [#2522] ### Changed + - Rename trait `IntoHeaderPair => TryIntoHeaderPair`. [#2510] - Rename `TryIntoHeaderPair::{try_into_header_pair => try_into_pair}`. [#2510] - Rename trait `IntoHeaderValue => TryIntoHeaderValue`. [#2510] ### Removed + - `MessageBody::{is_complete_body,take_complete_body}`. [#2522] [#2510]: https://github.com/actix/actix-web/pull/2510 [#2522]: https://github.com/actix/actix-web/pull/2522 - ## 3.0.0-beta.15 - 2021-12-11 + ### Added + - Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483] - HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] - `Response::map_into_boxed_body`. [#2468] @@ -530,6 +579,7 @@ - New `boxed` method on `MessageBody` trait for wrapping body type. [#2520] ### Changed + - Rename `body::BoxBody::{from_body => new}`. [#2468] - Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468] - The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468] @@ -539,6 +589,7 @@ - `ResponseBuilder::finish()` now returns `Response>`. [#2468] ### Removed + - `ResponseBuilder::streaming`. [#2468] - `impl Future` for `ResponseBuilder`. [#2468] - Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468] @@ -558,9 +609,10 @@ [#2497]: https://github.com/actix/actix-web/pull/2497 [#2520]: https://github.com/actix/actix-web/pull/2520 - ## 3.0.0-beta.14 - 2021-11-30 + ### Changed + - Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] - Expose `header::map` module. [#2467] - Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470] @@ -570,15 +622,17 @@ [#2470]: https://github.com/actix/actix-web/pull/2470 [#2474]: https://github.com/actix/actix-web/pull/2474 - ## 3.0.0-beta.13 - 2021-11-22 + ### Added + - `body::AnyBody::empty` for quickly creating an empty body. [#2446] - `body::AnyBody::none` for quickly creating a "none" body. [#2456] - `impl Clone` for `body::AnyBody where S: Clone`. [#2448] - `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448] ### Changed + - Rename `body::AnyBody::{Message => Body}`. [#2446] - Rename `body::AnyBody::{from_message => new_boxed}`. [#2448] - Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448] @@ -587,6 +641,7 @@ - `Encoder::response` now returns `AnyBody>`. [#2448] ### Removed + - `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] - `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] - `EncoderError::Boxed`; it is no longer required. [#2446] @@ -596,33 +651,38 @@ [#2448]: https://github.com/actix/actix-web/pull/2448 [#2456]: https://github.com/actix/actix-web/pull/2456 - ## 3.0.0-beta.12 - 2021-11-15 + ### Changed + - Update `actix-server` to `2.0.0-beta.9`. [#2442] ### Removed + - `client` module. [#2425] - `trust-dns` feature. [#2425] [#2425]: https://github.com/actix/actix-web/pull/2425 [#2442]: https://github.com/actix/actix-web/pull/2442 - ## 3.0.0-beta.11 - 2021-10-20 + ### Changed + - Updated rustls to v0.20. [#2414] - Minimum supported Rust version (MSRV) is now 1.52. [#2414]: https://github.com/actix/actix-web/pull/2414 - ## 3.0.0-beta.10 - 2021-09-09 + ### Changed + - `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] - Minimum supported Rust version (MSRV) is now 1.51. ### Fixed + - Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] - Remove `Into` bound on `Encoder` body types. [#2375] - Fix quality parse error in Accept-Encoding header. [#2344] @@ -632,25 +692,29 @@ [#2344]: https://github.com/actix/actix-web/pull/2344 [#2377]: https://github.com/actix/actix-web/pull/2377 - ## 3.0.0-beta.9 - 2021-08-09 + ### Fixed + - Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) - ## 3.0.0-beta.8 - 2021-06-26 + ### Changed + - Change compression algorithm features flags. [#2250] ### Removed + - `downcast` and `downcast_get_type_id` macros. [#2291] [#2291]: https://github.com/actix/actix-web/pull/2291 [#2250]: https://github.com/actix/actix-web/pull/2250 - ## 3.0.0-beta.7 - 2021-06-17 + ### Added + - Alias `body::Body` as `body::AnyBody`. [#2215] - `BoxAnyBody`: a boxed message body with boxed errors. [#2183] - Re-export `http` crate's `Error` type as `error::HttpError`. [#2171] @@ -661,6 +725,7 @@ - Add zstd support for `ContentEncoding`. [#2244] ### Changed + - The `MessageBody` trait now has an associated `Error` type. [#2183] - All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] - All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] @@ -671,9 +736,10 @@ - Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] - `ResponseBuilder::message_body` now returns a `Result`. [#2201] - Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253] -- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] +- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuration parameter. [#2226] ### Removed + - Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171] - Down-casting for `MessageBody` types. [#2183] - `error::Result` alias. [#2201] @@ -692,21 +758,23 @@ [#2253]: https://github.com/actix/actix-web/pull/2253 [#2244]: https://github.com/actix/actix-web/pull/2244 - - ## 3.0.0-beta.6 - 2021-04-17 + ### Added + - `impl MessageBody for Pin>`. [#2152] - `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159] - Helper `body::to_bytes` for async collecting message body into Bytes. [#2158] ### Changed + - The type parameter of `Response` no longer has a default. [#2152] - The `Message` variant of `body::Body` is now `Pin>`. [#2152] - `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152] - Error enum types are marked `#[non_exhaustive]`. [#2161] ### Removed + - `cookies` feature flag. [#2065] - Top-level `cookies` mod (re-export). [#2065] - `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065] @@ -725,17 +793,20 @@ [#2158]: https://github.com/actix/actix-web/pull/2158 [#2161]: https://github.com/actix/actix-web/pull/2161 - ## 3.0.0-beta.5 - 2021-04-02 + ### Added + - `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081] - `client::ConnectorService` as `client::Connector::finish` method's return type [#2081] - `client::ConnectionIo` trait alias [#2081] ### Changed + - `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063] ### Removed + - Common typed HTTP headers were moved to actix-web. [2094] - `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127] @@ -744,14 +815,16 @@ [#2094]: https://github.com/actix/actix-web/pull/2094 [#2127]: https://github.com/actix/actix-web/pull/2127 - ## 3.0.0-beta.4 - 2021-03-08 + ### Changed + - Feature `cookies` is now optional and disabled by default. [#1981] - `ws::hash_key` now returns array. [#2035] - `ResponseBuilder::json` now takes `impl Serialize`. [#2052] ### Removed + - Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994] - `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994] @@ -760,13 +833,14 @@ [#2035]: https://github.com/actix/actix-web/pull/2035 [#2052]: https://github.com/actix/actix-web/pull/2052 - ## 3.0.0-beta.3 - 2021-02-10 + - No notable changes. - ## 3.0.0-beta.2 - 2021-02-10 + ### Added + - `TryIntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869] - `ResponseBuilder::insert_header` method which allows using typed headers. [#1869] - `ResponseBuilder::append_header` method which allows using typed headers. [#1869] @@ -778,22 +852,21 @@ - `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] ### Changed -- `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed - `mime` types. [#1894] -- Renamed `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std - `TryInto` trait. [#1894] + +- `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed `mime` types. [#1894] +- Renamed `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std `TryInto` trait. [#1894] - `Extensions::insert` returns Option of replaced item. [#1904] - Remove `HttpResponseBuilder::json2()`. [#1903] - Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903] - `client::error::ConnectError` Resolver variant contains `Box` type. [#1905] - `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] -- Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool - is dead. [#1957] +- Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool is dead. [#1957] - `HeaderMap::len` now returns number of values instead of number of keys. [#1964] - `HeaderMap::insert` now returns iterator of removed values. [#1964] - `HeaderMap::remove` now returns iterator of removed values. [#1964] ### Removed + - `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869] - `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869] - `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869] @@ -803,6 +876,7 @@ - `ResponseError` impl for `actix::MailboxError`. [#1969] ### Documentation + - Vastly improve docs and add examples for `HeaderMap`. [#1964] [#1869]: https://github.com/actix/actix-web/pull/1869 @@ -815,12 +889,14 @@ [#1964]: https://github.com/actix/actix-web/pull/1964 [#1969]: https://github.com/actix/actix-web/pull/1969 - ## 3.0.0-beta.1 - 2021-01-07 + ### Added + - Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`. ### Changed + - Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] - Bumped `rand` to `0.8`. - Update `bytes` to `1.0`. [#1813] @@ -828,16 +904,11 @@ - The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864] ### Removed -- Deprecated `on_connect` methods have been removed. Prefer the new - `on_connect_ext` technique. [#1857] -- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError` - due to deprecate of resolver actor. [#1813] -- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. - due to the removal of this type from `tokio-openssl` crate. openssl handshake - error would return as `ConnectError::SslError`. [#1813] -- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`. - Due to this change `actix_threadpool::BlockingError` type is moved into - `actix_http::error` module. [#1878] + +- Deprecated `on_connect` methods have been removed. Prefer the new `on_connect_ext` technique. [#1857] +- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError` due to deprecate of resolver actor. [#1813] +- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. due to the removal of this type from `tokio-openssl` crate. openssl handshake error would return as `ConnectError::SslError`. [#1813] +- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`. Due to this change `actix_threadpool::BlockingError` type is moved into `actix_http::error` module. [#1878] [#1813]: https://github.com/actix/actix-web/pull/1813 [#1857]: https://github.com/actix/actix-web/pull/1857 @@ -846,29 +917,34 @@
- ## 2.2.2 - 2022-01-21 + ### Changed + - Migrate to `brotli` crate. [ad7e3c06] [ad7e3c06]: https://github.com/actix/actix-web/commit/ad7e3c06 - ## 2.2.1 - 2021-08-09 + ### Fixed + - Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) - ## 2.2.0 - 2020-11-25 + ### Added + - HttpResponse builders for 1xx status codes. [#1768] - `Accept::mime_precedence` and `Accept::mime_preference`. [#1793] - `TryFrom` and `TryFrom` for `http::header::Quality`. [#1797] ### Fixed + - Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767] ### Changed + - Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 @@ -877,12 +953,14 @@ [#1793]: https://github.com/actix/actix-web/pull/1793 [#1797]: https://github.com/actix/actix-web/pull/1797 - ## 2.1.0 - 2020-10-30 + ### Added + - Added more flexible `on_connect_ext` methods for on-connect handling. [#1754] ### Changed + - Upgrade `base64` to `0.13`. [#1744] - Upgrade `pin-project` to `1.0`. [#1733] - Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760] @@ -892,37 +970,42 @@ [#1733]: https://github.com/actix/actix-web/pull/1733 [#1744]: https://github.com/actix/actix-web/pull/1744 - ## 2.0.0 - 2020-09-11 + - No significant changes from `2.0.0-beta.4`. - ## 2.0.0-beta.4 - 2020-09-09 + ### Changed + - Update actix-codec and actix-utils dependencies. - Update actix-connect and actix-tls dependencies. - ## 2.0.0-beta.3 - 2020-08-14 + ### Fixed + - Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] [#1626]: https://github.com/actix/actix-web/pull/1626 - ## 2.0.0-beta.2 - 2020-07-21 + ### Fixed + - Potential UB in h1 decoder using uninitialized memory. [#1614] ### Changed + - Fix illegal chunked encoding. [#1615] [#1614]: https://github.com/actix/actix-web/pull/1614 [#1615]: https://github.com/actix/actix-web/pull/1615 - ## 2.0.0-beta.1 - 2020-07-11 + ### Changed + - Migrate cookie handling to `cookie` crate. [#1558] - Update `sha-1` to 0.9. [#1586] - Fix leak in client pool. [#1580] @@ -932,278 +1015,319 @@ [#1586]: https://github.com/actix/actix-web/pull/1586 [#1580]: https://github.com/actix/actix-web/pull/1580 - ## 2.0.0-alpha.4 - 2020-05-21 + ### Changed + - Bump minimum supported Rust version to 1.40 -- content_length function is removed, and you can set Content-Length by calling - no_chunking function [#1439] -- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a - `u64` instead of a `usize`. +- content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439] +- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`. - Update `base64` dependency to 0.12 ### Fixed + - Support parsing of `SameSite=None` [#1503] [#1439]: https://github.com/actix/actix-web/pull/1439 [#1503]: https://github.com/actix/actix-web/pull/1503 - ## 2.0.0-alpha.3 - 2020-05-08 + ### Fixed + - Correct spelling of ConnectError::Unresolved [#1487] -- Fix a mistake in the encoding of websocket continuation messages wherein - Item::FirstText and Item::FirstBinary are each encoded as the other. +- Fix a mistake in the encoding of websocket continuation messages wherein Item::FirstText and Item::FirstBinary are each encoded as the other. ### Changed + - Implement `std::error::Error` for our custom errors [#1422] -- Remove `failure` support for `ResponseError` since that crate - will be deprecated in the near future. +- Remove `failure` support for `ResponseError` since that crate will be deprecated in the near future. [#1422]: https://github.com/actix/actix-web/pull/1422 [#1487]: https://github.com/actix/actix-web/pull/1487 - ## 2.0.0-alpha.2 - 2020-03-07 + ### Changed + - Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395] -- Change default initial window size and connection window size for HTTP2 to 2MB and 1MB - respectively to improve download speed for awc when downloading large objects. [#1394] -- client::Connector accepts initial_window_size and initial_connection_window_size - HTTP2 configuration. [#1394] +- Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively to improve download speed for awc when downloading large objects. [#1394] +- client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394] - client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394] [#1394]: https://github.com/actix/actix-web/pull/1394 [#1395]: https://github.com/actix/actix-web/pull/1395 - ## 2.0.0-alpha.1 - 2020-02-27 + ### Changed + - Update the `time` dependency to 0.2.7. - Moved actors messages support from actix crate, enabled with feature `actors`. -- Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of - `&mut self` in the poll_next(). +- Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of `&mut self` in the poll_next(). - MessageBody is not implemented for &'static [u8] anymore. ### Fixed + - Allow `SameSite=None` cookies to be sent in a response. - ## 1.0.1 - 2019-12-20 + ### Fixed + - Poll upgrade service's readiness from HTTP service handlers - Replace brotli with brotli2 #1224 - ## 1.0.0 - 2019-12-13 + ### Added + - Add websockets continuation frame support ### Changed + - Replace `flate2-xxx` features with `compress` - ## 1.0.0-alpha.5 - 2019-12-09 + ### Fixed + - Check `Upgrade` service readiness before calling it - Fix buffer remaining capacity calculation ### Changed + - Websockets: Ping and Pong should have binary data #1049 - ## 1.0.0-alpha.4 - 2019-12-08 + ### Added + - Add impl ResponseBuilder for Error ### Changed + - Use rust based brotli compression library ## 1.0.0-alpha.3 - 2019-12-07 + ### Changed + - Migrate to tokio 0.2 - Migrate to `std::future` - ## 0.2.11 - 2019-11-06 + ### Added + - Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() -- 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) - Allow to use `std::convert::Infallible` as `actix_http::error::Error` ### Fixed -- To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; - charset=utf-8` header [#1118] + +- To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header [#1118] [#1878]: https://github.com/actix/actix-web/pull/1878 - ## 0.2.10 - 2019-09-11 + ### Added -- Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests - with `RequestHead` + +- Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests with `RequestHead` ### Fixed + - h2 will use error response #1080 - on_connect result isn't added to request extensions for http2 requests #1009 - ## 0.2.9 - 2019-08-13 + ### Changed + - Dropped the `byteorder`-dependency in favor of `stdlib`-implementation - Update percent-encoding to 2.1 - Update serde_urlencoded to 0.6.1 ### Fixed + - Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031) - ## 0.2.8 - 2019-08-01 + ### Added + - Add `rustls` support - Add `Clone` impl for `HeaderMap` ### Fixed -- awc client panic #1016 -- Invalid response with compression middleware enabled, but compression-related features - disabled #997 +- awc client panic #1016 +- Invalid response with compression middleware enabled, but compression-related features disabled #997 ## 0.2.7 - 2019-07-18 + ### Added + - Add support for downcasting response errors #986 - ## 0.2.6 - 2019-07-17 + ### Changed + - Replace `ClonableService` with local copy - Upgrade `rand` dependency version to 0.7 - ## 0.2.5 - 2019-06-28 + ### Added + - Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946 ### Changed + - Use `encoding_rs` crate instead of unmaintained `encoding` crate - Add `Copy` and `Clone` impls for `ws::Codec` - ## 0.2.4 - 2019-06-16 + ### Fixed + - Do not compress NoContent (204) responses #918 - ## 0.2.3 - 2019-06-02 + ### Added + - Debug impl for ResponseBuilder - From SizedStream and BodyStream for Body ### Changed + - SizedStream uses u64 - ## 0.2.2 - 2019-05-29 + ### Fixed + - Parse incoming stream before closing stream on disconnect #868 - ## 0.2.1 - 2019-05-25 + ### Fixed + - Handle socket read disconnect - ## 0.2.0 - 2019-05-12 + ### Changed + - Update actix-service to 0.4 - Expect and upgrade services accept `ServerConfig` config. ### Deleted + - `OneRequest` service - ## 0.1.5 - 2019-05-04 + ### Fixed + - Clean up response extensions in response pool #817 - ## 0.1.4 - 2019-04-24 + ### Added + - Allow to render h1 request headers in `Camel-Case` ### Fixed + - Read until eof for http/1.0 responses #771 - ## 0.1.3 - 2019-04-23 + ### Fixed + - Fix http client pool management - Fix http client wait queue management #794 - ## 0.1.2 - 2019-04-23 + ### Fixed + - Fix BorrowMutError panic in client connector #793 - ## 0.1.1 - 2019-04-19 + ### Changed + - Cookie::max_age() accepts value in seconds - Cookie::max_age_time() accepts value in time::Duration - Allow to specify server address for client connector - ## 0.1.0 - 2019-04-16 + ### Added + - Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` ### Changed + - `actix_http::encoding` always available - use trust-dns-resolver 0.11.0 - ## 0.1.0-alpha.5 - 2019-04-12 + ### Added + - Allow to use custom service for upgrade requests - Added `h1::SendResponse` future. ### Changed + - MessageBody::length() renamed to MessageBody::size() for consistency - ws handshake verification functions take RequestHead instead of Request - ## 0.1.0-alpha.4 - 2019-04-08 + ### Added + - Allow to use custom `Expect` handler - Add minimal `std::error::Error` impl for `Error` ### Changed + - Export IntoHeaderValue - Render error and return as response body - Use thread pool for response body compression ### Deleted + - Removed PayloadBuffer - ## 0.1.0-alpha.3 - 2019-04-02 + ### Added + - Warn when an unsealed private cookie isn't valid UTF-8 ### Fixed + - Rust 1.31.0 compatibility - Preallocate read buffer for h1 codec - Detect socket disconnection during protocol selection - ## 0.1.0-alpha.2 - 2019-03-29 + ### Added + - Added ws::Message::Nop, no-op websockets message ### Changed + - Do not use thread pool for decompression if chunk size is smaller than 2048. - ## 0.1.0-alpha.1 - 2019-03-28 + - Initial impl diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 9f3977f0f..108b2314a 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.2.2" +version = "3.3.0" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -61,7 +61,7 @@ actix-codec = "0.5" actix-utils = "3" actix-rt = { version = "2.2", default-features = false } -ahash = "0.7" +ahash = "0.8" bitflags = "1.2" bytes = "1" bytestring = "1" @@ -77,7 +77,7 @@ mime = "0.3" percent-encoding = "2.1" pin-project-lite = "0.2" smallvec = "1.6.1" -tokio = { version = "1.13.1", features = [] } +tokio = { version = "1.24.2", features = [] } tokio-util = { version = "0.7", features = ["io", "codec"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] } @@ -86,7 +86,7 @@ h2 = { version = "0.3.9", optional = true } # websockets 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 } sha1 = { version = "0.10", optional = true } @@ -119,7 +119,7 @@ serde_json = "1.0" static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -tokio = { version = "1.8.4", features = ["net", "rt", "macros"] } +tokio = { version = "1.24.2", features = ["net", "rt", "macros"] } [[example]] name = "ws" diff --git a/actix-http/README.md b/actix-http/README.md index 994cf97c6..aa98f953f 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,18 +3,18 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.2.2)](https://docs.rs/actix-http/3.2.2) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.3.0)](https://docs.rs/actix-http/3.3.0) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.2.2/status.svg)](https://deps.rs/crate/actix-http/3.2.2) +[![dependency status](https://deps.rs/crate/actix-http/3.3.0/status.svg)](https://deps.rs/crate/actix-http/3.3.0) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http) -- Minimum Supported Rust Version (MSRV): 1.54 +- Minimum Supported Rust Version (MSRV): 1.59 ## Example @@ -49,18 +49,3 @@ async fn main() -> io::Result<()> { .await } ``` - -## License - -This project is licensed under either of - -- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) - -at your option. - -## Code of Conduct - -Contribution to the actix-http crate is organized under the terms of the -Contributor Covenant, the maintainer of actix-http, @fafhrd91, promises to -intervene to uphold that code of conduct. diff --git a/actix-http/src/h1/dispatcher_tests.rs b/actix-http/src/h1/dispatcher_tests.rs index d39c5bd69..db46cdefc 100644 --- a/actix-http/src/h1/dispatcher_tests.rs +++ b/actix-http/src/h1/dispatcher_tests.rs @@ -932,7 +932,6 @@ fn http_msg(msg: impl AsRef) -> BytesMut { .as_ref() .trim() .split('\n') - .into_iter() .map(|line| [line.trim_start(), "\r"].concat()) .collect::>() .join("\n"); diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 680936f0f..3e618820e 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -29,7 +29,7 @@ use crate::{ HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, }, service::HttpFlow, - Extensions, OnConnectData, Payload, Request, Response, ResponseHead, + Extensions, Method, OnConnectData, Payload, Request, Response, ResponseHead, }; const CHUNK_SIZE: usize = 16_384; @@ -118,6 +118,7 @@ where let payload = crate::h2::Payload::new(body); let pl = Payload::H2 { payload }; let mut req = Request::with_payload(pl); + let head_req = parts.method == Method::HEAD; let head = req.head_mut(); head.uri = parts.uri; @@ -135,10 +136,10 @@ where actix_rt::spawn(async move { // resolve service call and send response. 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) => { let res: Response = err.into(); - handle_response(res, tx, config).await + handle_response(res, tx, config, head_req).await } }; @@ -206,6 +207,7 @@ async fn handle_response( res: Response, mut tx: SendResponse, config: ServiceConfig, + head_req: bool, ) -> Result<(), DispatchError> where B: MessageBody, @@ -215,14 +217,14 @@ where // prepare response. let mut size = body.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. let mut stream = tx - .send_response(res, eof) + .send_response(res, eof_or_head) .map_err(DispatchError::SendResponse)?; - if eof { + if eof_or_head { return Ok(()); } diff --git a/actix-http/src/header/common.rs b/actix-http/src/header/common.rs index 52909099a..67b0a9069 100644 --- a/actix-http/src/header/common.rs +++ b/actix-http/src/header/common.rs @@ -4,6 +4,18 @@ 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. +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. +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]). /// diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index e2c2fe912..a63174a92 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -55,8 +55,9 @@ pub use self::{ // re-export list is explicit so that any updates to `http` do not conflict with this set pub use self::common::{ - CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY, CROSS_ORIGIN_RESOURCE_POLICY, - PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO, + CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY, + CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST, + X_FORWARDED_PROTO, }; /// An interface for types that already represent a valid header. diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 7222168b7..0653c00b0 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -3,6 +3,7 @@ use std::{ fmt, }; +use base64::prelude::*; use tracing::error; /// 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 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); hash_b64 diff --git a/actix-multipart-derive/Cargo.toml b/actix-multipart-derive/Cargo.toml new file mode 100644 index 000000000..4a30898b4 --- /dev/null +++ b/actix-multipart-derive/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "actix-multipart-derive" +version = "0.5.0" +authors = ["Jacob Halsey "] +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" diff --git a/actix-multipart-derive/LICENSE-APACHE b/actix-multipart-derive/LICENSE-APACHE new file mode 120000 index 000000000..965b606f3 --- /dev/null +++ b/actix-multipart-derive/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-multipart-derive/LICENSE-MIT b/actix-multipart-derive/LICENSE-MIT new file mode 120000 index 000000000..76219eb72 --- /dev/null +++ b/actix-multipart-derive/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-multipart-derive/README.md b/actix-multipart-derive/README.md new file mode 100644 index 000000000..95f80bc79 --- /dev/null +++ b/actix-multipart-derive/README.md @@ -0,0 +1,3 @@ +# actix-multipart-derive + +> The derive macro implementation for actix-multipart. diff --git a/actix-multipart-derive/src/lib.rs b/actix-multipart-derive/src/lib.rs new file mode 100644 index 000000000..9b6ecbae6 --- /dev/null +++ b/actix-multipart-derive/src/lib.rs @@ -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, + limit: Option, +} + +struct ParsedField<'t> { + serialization_name: String, + rust_name: &'t Ident, + limit: Option, + 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, +/// timestamp: Text, +/// image: TempFile, +/// } +/// ``` +/// +/// # Optional and List Fields +/// +/// You can also use `Vec` and `Option` 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>, +/// files: Vec, +/// } +/// ``` +/// +/// # 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, +/// } +/// ``` +/// +/// # Field Limits +/// +/// You can use the `#[multipart(limit = "")]` 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, +/// +/// #[multipart(limit = "512 MiB")] +/// files: Vec, +/// } +/// ``` +/// +/// # 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 = "")]` 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::, 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 { + 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> + '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 { + 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()) +} diff --git a/actix-multipart-derive/tests/trybuild.rs b/actix-multipart-derive/tests/trybuild.rs new file mode 100644 index 000000000..7b9f14ed7 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild.rs @@ -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"); +} diff --git a/actix-multipart-derive/tests/trybuild/all-required.rs b/actix-multipart-derive/tests/trybuild/all-required.rs new file mode 100644 index 000000000..1b4a824d9 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/all-required.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, + timestamp: Text, + image: TempFile, +} + +async fn handler(_form: MultipartForm) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/deny-duplicates.rs b/actix-multipart-derive/tests/trybuild/deny-duplicates.rs new file mode 100644 index 000000000..9fcc1506c --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/deny-duplicates.rs @@ -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
) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/deny-parse-fail.rs b/actix-multipart-derive/tests/trybuild/deny-parse-fail.rs new file mode 100644 index 000000000..5ea566fb0 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/deny-parse-fail.rs @@ -0,0 +1,7 @@ +use actix_multipart::form::MultipartForm; + +#[derive(MultipartForm)] +#[multipart(duplicate_field = "no")] +struct Form {} + +fn main() {} diff --git a/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr b/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr new file mode 100644 index 000000000..d25e43525 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr @@ -0,0 +1,5 @@ +error: Unknown literal value `no` + --> tests/trybuild/deny-parse-fail.rs:4:31 + | +4 | #[multipart(duplicate_field = "no")] + | ^^^^ diff --git a/actix-multipart-derive/tests/trybuild/deny-unknown.rs b/actix-multipart-derive/tests/trybuild/deny-unknown.rs new file mode 100644 index 000000000..e03460624 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/deny-unknown.rs @@ -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) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/optional-and-list.rs b/actix-multipart-derive/tests/trybuild/optional-and-list.rs new file mode 100644 index 000000000..deef3de59 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/optional-and-list.rs @@ -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>, + files: Vec, +} + +async fn handler(_form: MultipartForm) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/rename.rs b/actix-multipart-derive/tests/trybuild/rename.rs new file mode 100644 index 000000000..1f66bbb43 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/rename.rs @@ -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, +} + +async fn handler(_form: MultipartForm) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.rs b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.rs new file mode 100644 index 000000000..c3d495317 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.rs @@ -0,0 +1,21 @@ +use actix_multipart::form::{text::Text, MultipartForm}; + +#[derive(MultipartForm)] +struct Form { + #[multipart(limit = "2 bytes")] + description: Text, +} + +#[derive(MultipartForm)] +struct Form2 { + #[multipart(limit = "2 megabytes")] + description: Text, +} + +#[derive(MultipartForm)] +struct Form3 { + #[multipart(limit = "four meters")] + description: Text, +} + +fn main() {} diff --git a/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr new file mode 100644 index 000000000..fc02a78c4 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr @@ -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, + | ^^^^^^^^^^^ + +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, + | ^^^^^^^^^^^ + +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, + | ^^^^^^^^^^^ diff --git a/actix-multipart-derive/tests/trybuild/size-limits.rs b/actix-multipart-derive/tests/trybuild/size-limits.rs new file mode 100644 index 000000000..92c3d0db5 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/size-limits.rs @@ -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, + + #[multipart(limit = "512 MiB")] + files: Vec, +} + +async fn handler(_form: MultipartForm) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 655487e54..3d93a4921 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,39 +1,46 @@ # Changes ## 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. -- `Field::content_type()` now returns `Option<&mime::Mime>` [#2880] - -[#2880]: https://github.com/actix/actix-web/pull/2880 +[#2885]: https://github.com/actix/actix-web/pull/2885 ## 0.4.0 - 2022-02-25 + - No significant changes since `0.4.0-beta.13`. - ## 0.4.0-beta.13 - 2022-01-31 + - No significant changes since `0.4.0-beta.12`. - ## 0.4.0-beta.12 - 2022-01-04 + - Minimum supported Rust version (MSRV) is now 1.54. - ## 0.4.0-beta.11 - 2021-12-27 + - No significant changes since `0.4.0-beta.10`. - ## 0.4.0-beta.10 - 2021-12-11 + - No significant changes since `0.4.0-beta.9`. - ## 0.4.0-beta.9 - 2021-12-01 + - Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463] [#2463]: https://github.com/actix/actix-web/pull/2463 - ## 0.4.0-beta.8 - 2021-11-22 + - Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] - Added `MultipartError::NoContentDisposition` variant. [#2451] - Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451] @@ -43,52 +50,52 @@ [#2451]: https://github.com/actix/actix-web/pull/2451 - ## 0.4.0-beta.7 - 2021-10-20 + - Minimum supported Rust version (MSRV) is now 1.52. - ## 0.4.0-beta.6 - 2021-09-09 + - Minimum supported Rust version (MSRV) is now 1.51. - ## 0.4.0-beta.5 - 2021-06-17 -- No notable changes. +- No notable changes. ## 0.4.0-beta.4 - 2021-04-02 -- No notable changes. +- No notable changes. ## 0.4.0-beta.3 - 2021-03-09 -- No notable changes. +- No notable changes. ## 0.4.0-beta.2 - 2021-02-10 + - No notable changes. - ## 0.4.0-beta.1 - 2021-01-07 + - Fix multipart consuming payload before header checks. [#1513] - Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 [#1513]: https://github.com/actix/actix-web/pull/1513 - ## 0.3.0 - 2020-09-11 + - No significant changes from `0.3.0-beta.2`. - ## 0.3.0-beta.2 - 2020-09-10 + - Update `actix-*` dependencies to latest versions. - ## 0.3.0-beta.1 - 2020-07-15 + - Update `actix-web` to 3.0.0-beta.1 - ## 0.3.0-alpha.1 - 2020-05-25 + - Update `actix-web` to 3.0.0-alpha.3 - Bump minimum supported Rust version to 1.40 - Minimize `futures` dependencies diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 3226850db..b63ab1f86 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "actix-multipart" -version = "0.4.0" -authors = ["Nikolay Kim "] +version = "0.5.0" +authors = [ + "Nikolay Kim ", + "Jacob Halsey ", +] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" @@ -9,26 +12,46 @@ repository = "https://github.com/actix/actix-web.git" license = "MIT OR Apache-2.0" edition = "2018" +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true + +[features] +default = ["tempfile", "derive"] +derive = ["actix-multipart-derive"] +tempfile = ["tempfile-dep", "tokio/fs"] + [lib] name = "actix_multipart" path = "src/lib.rs" [dependencies] +actix-multipart-derive = { version = "=0.5.0", optional = true } actix-utils = "3" actix-web = { version = "4", default-features = false } bytes = "1" derive_more = "0.99.5" futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } +futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } httparse = "1.3" local-waker = "0.1" log = "0.4" -mime = "0.3" memchr = "2.5" +mime = "0.3" +serde = "1" +serde_json = "1" +serde_plain = "1" +# TODO(MSRV 1.60): replace with dep: prefix +tempfile-dep = { package = "tempfile", version = "3.4", optional = true } +tokio = { version = "1.24.2", features = ["sync"] } [dev-dependencies] -actix-rt = "2.2" actix-http = "3" +actix-multipart-rfc7578 = "0.10" +actix-rt = "2.2" +actix-test = "0.1" +awc = "3" futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } -tokio = { version = "1.8.4", features = ["sync"] } +tokio = { version = "1.24.2", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 21999716c..23c7aa4f5 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,15 +3,15 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0)](https://docs.rs/actix-multipart/0.4.0) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.5.0)](https://docs.rs/actix-multipart/0.5.0) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0) +[![dependency status](https://deps.rs/crate/actix-multipart/0.5.0/status.svg)](https://deps.rs/crate/actix-multipart/0.5.0) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-multipart) -- Minimum Supported Rust Version (MSRV): 1.54 +- Minimum Supported Rust Version (MSRV): 1.59 diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index 7d0da35e0..77b5a559f 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -1,12 +1,15 @@ //! Error and Result module -use actix_web::error::{ParseError, PayloadError}; -use actix_web::http::StatusCode; -use actix_web::ResponseError; + +use actix_web::{ + error::{ParseError, PayloadError}, + http::StatusCode, + ResponseError, +}; use derive_more::{Display, Error, From}; -/// A set of errors that can occur during parsing multipart streams -#[non_exhaustive] +/// A set of errors that can occur during parsing multipart streams. #[derive(Debug, Display, From, Error)] +#[non_exhaustive] pub enum MultipartError { /// Content-Disposition header is not found or is not equal to "form-data". /// @@ -46,12 +49,41 @@ pub enum MultipartError { /// Not consumed #[display(fmt = "Multipart stream is not consumed")] NotConsumed, + + /// An error from a field handler in a form + #[display( + fmt = "An error occurred processing field `{}`: {}", + field_name, + source + )] + Field { + field_name: String, + source: actix_web::Error, + }, + + /// Duplicate field + #[display(fmt = "Duplicate field found for: `{}`", _0)] + #[from(ignore)] + DuplicateField(#[error(not(source))] String), + + /// Missing field + #[display(fmt = "Field with name `{}` is required", _0)] + #[from(ignore)] + MissingField(#[error(not(source))] String), + + /// Unknown field + #[display(fmt = "Unsupported field `{}`", _0)] + #[from(ignore)] + UnsupportedField(#[error(not(source))] String), } /// Return `BadRequest` for `MultipartError` impl ResponseError for MultipartError { fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST + match &self { + MultipartError::Field { source, .. } => source.as_response_error().status_code(), + _ => StatusCode::BAD_REQUEST, + } } } diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index d45c4869c..56ed69ae4 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -9,8 +9,7 @@ use crate::server::Multipart; /// /// Content-type: multipart/form-data; /// -/// ## Server example -/// +/// # Examples /// ``` /// use actix_web::{web, HttpResponse, Error}; /// use actix_multipart::Multipart; diff --git a/actix-multipart/src/form/bytes.rs b/actix-multipart/src/form/bytes.rs new file mode 100644 index 000000000..7d64fffce --- /dev/null +++ b/actix-multipart/src/form/bytes.rs @@ -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, + + /// The `filename` value in the `Content-Disposition` header. + pub file_name: Option, +} + +impl<'t> FieldReader<'t> for Bytes { + type Future = LocalBoxFuture<'t, Result>; + + 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), + }) + }) + } +} diff --git a/actix-multipart/src/form/json.rs b/actix-multipart/src/form/json.rs new file mode 100644 index 000000000..9951eaaaf --- /dev/null +++ b/actix-multipart/src/form/json.rs @@ -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(pub T); + +impl Json { + pub fn into_inner(self) -> T { + self.0 + } +} + +impl<'t, T> FieldReader<'t> for Json +where + T: DeserializeOwned + 'static, +{ + type Future = LocalBoxFuture<'t, Result>; + + 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, + validate_content_type: bool, +} + +const DEFAULT_CONFIG: JsonConfig = JsonConfig { + err_handler: None, + validate_content_type: true, +}; + +impl JsonConfig { + pub fn error_handler(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`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().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>, + } + + async fn test_json_route(form: MultipartForm) -> 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); + } +} diff --git a/actix-multipart/src/form/mod.rs b/actix-multipart/src/form/mod.rs new file mode 100644 index 000000000..b0285d97e --- /dev/null +++ b/actix-multipart/src/form/mod.rs @@ -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 = Option 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>; + + /// 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>); + +/// Trait that the field collection types implement, i.e. `Vec`, `Option`, or `T` itself. +#[doc(hidden)] +pub trait FieldGroupReader<'t>: Sized + Any { + type Future: Future>; + + /// 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; +} + +impl<'t, T> FieldGroupReader<'t> for Option +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 { + Ok(state.remove(name).map(|m| *m.downcast::().unwrap())) + } +} + +impl<'t, T> FieldGroupReader<'t> for Vec +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::>::default()) + .downcast_mut::>() + .unwrap(); + + let item = T::read_field(req, field, limits).await?; + vec.push(item); + + Ok(()) + }) + } + + fn from_state(name: &str, state: &'t mut State) -> Result { + Ok(state + .remove(name) + .map(|m| *m.downcast::>().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 { + state + .remove(name) + .map(|m| *m.downcast::().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; + + /// 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; +} + +#[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, +} + +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(pub T); + +impl MultipartForm { + /// Unwrap into inner `T` value. + pub fn into_inner(self) -> T { + self.0 + } +} + +impl FromRequest for MultipartForm +where + T: MultipartCollect, +{ + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + #[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::>::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 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(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`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().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> { + 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>, + field2: Option>, + } + + async fn test_options_route(form: MultipartForm) -> 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>, + list2: Vec>, + } + + async fn test_vec_route(form: MultipartForm) -> impl Responder { + let form = form.into_inner(); + let strings = form + .list1 + .into_iter() + .map(|s| s.into_inner()) + .collect::>(); + 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, + #[multipart(rename = "field1")] + field2: Text, + field3: Text, + } + + async fn test_field_renaming_route( + form: MultipartForm, + ) -> 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) -> impl Responder { + HttpResponse::Ok().finish() + } + + async fn test_allow_unknown_route(_: MultipartForm) -> 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, + } + + #[derive(MultipartForm)] + #[multipart(duplicate_field = "replace")] + struct TestDuplicateReplace { + field: Text, + } + + #[derive(MultipartForm)] + #[multipart(duplicate_field = "ignore")] + struct TestDuplicateIgnore { + field: Text, + } + + async fn test_duplicate_deny_route(_: MultipartForm) -> impl Responder { + HttpResponse::Ok().finish() + } + + async fn test_duplicate_replace_route( + form: MultipartForm, + ) -> impl Responder { + assert_eq!(&*form.field, "second_value"); + HttpResponse::Ok().finish() + } + + async fn test_duplicate_ignore_route( + form: MultipartForm, + ) -> 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, + ) -> impl Responder { + assert!(!form.field.data.is_empty()); + HttpResponse::Ok().finish() + } + + async fn test_upload_limits_file( + form: MultipartForm, + ) -> 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, + } + + async fn test_field_level_limits_route( + form: MultipartForm, + ) -> 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); + } +} diff --git a/actix-multipart/src/form/tempfile.rs b/actix-multipart/src/form/tempfile.rs new file mode 100644 index 000000000..3c637e717 --- /dev/null +++ b/actix-multipart/src/form/tempfile.rs @@ -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, + + /// The `filename` value in the `content-disposition` header. + pub file_name: Option, + + /// The size in bytes of the file. + pub size: usize, +} + +impl<'t> FieldReader<'t> for TempFile { + type Future = LocalBoxFuture<'t, Result>; + + 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, + directory: Option, +} + +impl TempFileConfig { + fn create_tempfile(&self) -> io::Result { + if let Some(ref dir) = self.directory { + NamedTempFile::new_in(dir) + } else { + NamedTempFile::new() + } + } +} + +impl TempFileConfig { + /// Sets custom error handler. + pub fn error_handler(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`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().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) -> 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) -> 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); + } +} diff --git a/actix-multipart/src/form/text.rs b/actix-multipart/src/form/text.rs new file mode 100644 index 000000000..83e211524 --- /dev/null +++ b/actix-multipart/src/form/text.rs @@ -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(pub T); + +impl Text { + /// Unwraps into inner value. + pub fn into_inner(self) -> T { + self.0 + } +} + +impl<'t, T> FieldReader<'t> for Text +where + T: DeserializeOwned + 'static, +{ + type Future = LocalBoxFuture<'t, Result>; + + 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, + validate_content_type: bool, +} + +impl TextConfig { + /// Sets custom error handler. + pub fn error_handler(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`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().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, + } + + async fn test_text_route(form: MultipartForm) -> 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); + } +} diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index 37d03db49..c8fba77d0 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -3,10 +3,17 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] #![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 extractor; mod server; +pub mod form; + pub use self::error::MultipartError; pub use self::server::{Field, Multipart}; diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 9e0becd5c..6726bc9d3 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -270,7 +270,9 @@ impl InnerMultipart { match field.borrow_mut().poll(safety) { Poll::Pending => return Poll::Pending, Poll::Ready(Some(Ok(_))) => continue, - Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))), + Poll::Ready(Some(Err(err))) => { + return Poll::Ready(Some(Err(err))) + } Poll::Ready(None) => true, } } @@ -658,7 +660,7 @@ impl InnerField { match res { Poll::Pending => return Poll::Pending, Poll::Ready(Some(Ok(bytes))) => return Poll::Ready(Some(Ok(bytes))), - Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))), + Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), Poll::Ready(None) => self.eof = true, } } @@ -673,7 +675,7 @@ impl InnerField { } Poll::Ready(None) } - Err(e) => Poll::Ready(Some(Err(e))), + Err(err) => Poll::Ready(Some(Err(err))), } } else { Poll::Pending @@ -794,7 +796,7 @@ impl PayloadBuffer { loop { match Pin::new(&mut self.stream).poll_next(cx) { Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data), - Poll::Ready(Some(Err(e))) => return Err(e), + Poll::Ready(Some(Err(err))) => return Err(err), Poll::Ready(None) => { self.eof = true; return Ok(()); @@ -860,19 +862,22 @@ impl PayloadBuffer { #[cfg(test)] mod tests { - use super::*; + use std::time::Duration; - use actix_http::h1::Payload; - use actix_web::http::header::{DispositionParam, DispositionType}; - use actix_web::rt; - use actix_web::test::TestRequest; - use actix_web::FromRequest; + use actix_http::h1; + use actix_web::{ + http::header::{DispositionParam, DispositionType}, + rt, + test::TestRequest, + FromRequest, + }; use bytes::Bytes; use futures_util::{future::lazy, StreamExt as _}; - use std::time::Duration; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; + use super::*; + #[actix_rt::test] async fn test_boundary() { let headers = HeaderMap::new(); @@ -1119,7 +1124,7 @@ mod tests { #[actix_rt::test] async fn test_basic() { - let (_, payload) = Payload::create(false); + let (_, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(payload.buf.len(), 0); @@ -1129,7 +1134,7 @@ mod tests { #[actix_rt::test] async fn test_eof() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_max(4).unwrap()); @@ -1145,7 +1150,7 @@ mod tests { #[actix_rt::test] async fn test_err() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_max(1).unwrap()); sender.set_error(PayloadError::Incomplete(None)); @@ -1154,7 +1159,7 @@ mod tests { #[actix_rt::test] async fn test_readmax() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); sender.feed_data(Bytes::from("line1")); @@ -1171,7 +1176,7 @@ mod tests { #[actix_rt::test] async fn test_readexactly() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_exact(2)); @@ -1189,7 +1194,7 @@ mod tests { #[actix_rt::test] async fn test_readuntil() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_until(b"ne").unwrap()); @@ -1230,7 +1235,7 @@ mod tests { #[actix_rt::test] async fn test_multipart_payload_consumption() { // with sample payload and HttpRequest with no headers - let (_, inner_payload) = Payload::create(false); + let (_, inner_payload) = h1::Payload::create(false); let mut payload = actix_web::dev::Payload::from(inner_payload); let req = TestRequest::default().to_http_request(); diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 51e7cbc10..b3a85d3fe 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -2,16 +2,17 @@ ## Unreleased - 2022-xx-xx - ## 0.5.1 - 2022-09-19 + - Correct typo in error string for `i32` deserialization. [#2876] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2876]: https://github.com/actix/actix-web/pull/2876 - ## 0.5.0 - 2022-02-22 + ### Added + - Add `Path::as_str`. [#2590] - Add `ResourceDef::set_name`. [#373][net#373] - Add `RouterBuilder::push`. [#2612] @@ -23,6 +24,7 @@ - Support multi-pattern prefixes and joins. [#2356] ### Changed + - Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612] - Deprecate `Path::path`. [#2590] - Disallow prefix routes with tail segments. [#379][net#379] @@ -47,6 +49,7 @@ - Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373] ### Fixed + - Fix `ResourceDef`'s `PartialEq` implementation. [#373][net#373] - Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368] - Improve malformed path error message. [#384][net#384] @@ -55,6 +58,7 @@ - Static patterns in multi-patterns are no longer interpreted as regex. [#366][net#366] ### Removed + - `ResourceDef::name_mut`. [#373][net#373] - Unused `ResourceInfo`. [#2612] @@ -77,11 +81,11 @@ [net#380]: https://github.com/actix/actix-net/pull/380 [net#384]: https://github.com/actix/actix-net/pull/384 -
0.5.0 Pre-Releases ## 0.5.0-rc.3 - 2022-01-31 + - Remove unused `ResourceInfo`. [#2612] - Add `RouterBuilder::push`. [#2612] - Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612] @@ -92,33 +96,33 @@ [#2612]: https://github.com/actix/actix-web/pull/2612 [#2613]: https://github.com/actix/actix-web/pull/2613 - ## 0.5.0-rc.2 - 2022-01-21 + - Add `Path::as_str`. [#2590] - Deprecate `Path::path`. [#2590] [#2590]: https://github.com/actix/actix-web/pull/2590 - ## 0.5.0-rc.1 - 2022-01-14 + - `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568] - `Resource` is now implemented for `&mut Path<_>` and `RefMut>`. [#2568] [#2568]: https://github.com/actix/actix-web/pull/2568 - ## 0.5.0-beta.4 - 2022-01-04 + - `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566] - Minimum supported Rust version (MSRV) is now 1.54. [#2566]: https://github.com/actix/actix-net/pull/2566 - ## 0.5.0-beta.3 - 2021-12-17 + - Minimum supported Rust version (MSRV) is now 1.52. - ## 0.5.0-beta.2 - 2021-09-09 + - Introduce `ResourceDef::join`. [#380][net#380] - Disallow prefix routes with tail segments. [#379][net#379] - Enforce path separators on dynamic prefixes. [#378][net#378] @@ -137,8 +141,8 @@ [#2355]: https://github.com/actix/actix-web/pull/2355 [#2356]: https://github.com/actix/actix-web/pull/2356 - ## 0.5.0-beta.1 - 2021-07-20 + - Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366][net#366] - Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373] - Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368] @@ -167,8 +171,8 @@
- ## 0.4.0 - 2021-06-06 + - When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357][net#357] - Path tail patterns now match new lines (`\n`) in request URL. [#360][net#360] - Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359][net#359] @@ -179,70 +183,70 @@ [net#359]: https://github.com/actix/actix-net/pull/359 [net#360]: https://github.com/actix/actix-net/pull/360 - ## 0.3.0 - 2019-12-31 + - Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 - ## 0.2.7 - 2021-02-06 + - Add `Router::recognize_checked` [#247][net#247] [net#247]: https://github.com/actix/actix-net/pull/247 - ## 0.2.6 - 2021-01-09 + - Use `bytestring` version range compatible with Bytes v1.0. [#246][net#246] [net#246]: https://github.com/actix/actix-net/pull/246 - ## 0.2.5 - 2020-09-20 + - Fix `from_hex()` method - ## 0.2.4 - 2019-12-31 + - Add `ResourceDef::resource_path_named()` path generation method - ## 0.2.3 - 2019-12-25 + - Add impl `IntoPattern` for `&String` - ## 0.2.2 - 2019-12-25 + - Use `IntoPattern` for `RouterBuilder::path()` - ## 0.2.1 - 2019-12-25 + - Add `IntoPattern` trait - Add multi-pattern resources - ## 0.2.0 - 2019-12-07 + - Update http to 0.2 - Update regex to 1.3 - Use bytestring instead of string - ## 0.1.5 - 2019-05-15 + - Remove debug prints - ## 0.1.4 - 2019-05-15 + - Fix checked resource match - ## 0.1.3 - 2019-04-22 -- Added support for `remainder match` (i.e "/path/{tail}*") +- Added support for `remainder match` (i.e "/path/{tail}\*") ## 0.1.2 - 2019-04-07 + - Export `Quoter` type - Allow to reset `Path` instance - ## 0.1.1 - 2019-04-03 + - Get dynamic segment by name instead of iterator. - ## 0.1.0 - 2019-03-09 + - Initial release diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index a2733b1fb..ba8bc8a9a 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,73 +1,74 @@ # Changes ## Unreleased - 2022-xx-xx + - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. - Ability to set port number in TestServerConfig - ## 0.1.0 - 2022-07-24 + - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - ## 0.1.0-beta.13 - 2022-02-16 + - No significant changes since `0.1.0-beta.12`. - ## 0.1.0-beta.12 - 2022-01-31 + - Rename `TestServerConfig::{client_timeout => client_request_timeout}`. [#2611] [#2611]: https://github.com/actix/actix-web/pull/2611 - ## 0.1.0-beta.11 - 2022-01-04 + - Minimum supported Rust version (MSRV) is now 1.54. - ## 0.1.0-beta.10 - 2021-12-27 + - No significant changes since `0.1.0-beta.9`. - ## 0.1.0-beta.9 - 2021-12-17 + - Re-export `actix_http::body::to_bytes`. [#2518] - Update `actix_web::test` re-exports. [#2518] [#2518]: https://github.com/actix/actix-web/pull/2518 - ## 0.1.0-beta.8 - 2021-12-11 + - No significant changes since `0.1.0-beta.7`. - ## 0.1.0-beta.7 - 2021-11-22 + - Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 - ## 0.1.0-beta.6 - 2021-11-15 + - No significant changes from `0.1.0-beta.5`. - ## 0.1.0-beta.5 - 2021-10-20 + - Updated rustls to v0.20. [#2414] - Minimum supported Rust version (MSRV) is now 1.52. [#2414]: https://github.com/actix/actix-web/pull/2414 - ## 0.1.0-beta.4 - 2021-09-09 + - Minimum supported Rust version (MSRV) is now 1.51. - ## 0.1.0-beta.3 - 2021-06-20 + - No significant changes from `0.1.0-beta.2`. - ## 0.1.0-beta.2 - 2021-04-17 + - No significant changes from `0.1.0-beta.1`. - ## 0.1.0-beta.1 - 2021-04-02 + - Move integration testing structs from `actix-web`. [#2112] [#2112]: https://github.com/actix/actix-web/pull/2112 diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 05d12c25a..956005d8a 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -45,4 +45,4 @@ serde_json = "1" serde_urlencoded = "0.7" tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true } -tokio = { version = "1.8.4", features = ["sync"] } +tokio = { version = "1.24.2", features = ["sync"] } diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 33d4712f8..480470bee 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,70 +1,73 @@ # Changes ## Unreleased - 2022-xx-xx + +## 4.2.0 - 2023-01-21 + - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - ## 4.1.0 - 2022-03-02 + - Add support for `actix` version `0.13`. [#2675] [#2675]: https://github.com/actix/actix-web/pull/2675 - ## 4.0.0 - 2022-02-25 + - No significant changes since `4.0.0-beta.12`. - ## 4.0.0-beta.12 - 2022-02-16 + - No significant changes since `4.0.0-beta.11`. - ## 4.0.0-beta.11 - 2022-01-31 + - No significant changes since `4.0.0-beta.10`. - ## 4.0.0-beta.10 - 2022-01-04 + - Minimum supported Rust version (MSRV) is now 1.54. - ## 4.0.0-beta.9 - 2021-12-27 + - No significant changes since `4.0.0-beta.8`. - ## 4.0.0-beta.8 - 2021-12-11 + - Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920] - Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920] - Minimum supported Rust version (MSRV) is now 1.52. [#1920]: https://github.com/actix/actix-web/pull/1920 - ## 4.0.0-beta.7 - 2021-09-09 + - Minimum supported Rust version (MSRV) is now 1.51. - ## 4.0.0-beta.6 - 2021-06-26 + - Update `actix` to `0.12`. [#2277] [#2277]: https://github.com/actix/actix-web/pull/2277 - ## 4.0.0-beta.5 - 2021-06-17 -- No notable changes. +- No notable changes. ## 4.0.0-beta.4 - 2021-04-02 -- No notable changes. +- No notable changes. ## 4.0.0-beta.3 - 2021-03-09 -- No notable changes. +- No notable changes. ## 4.0.0-beta.2 - 2021-02-10 + - No notable changes. - ## 4.0.0-beta.1 - 2021-01-07 + - Update `pin-project` to `1.0`. - Update `bytes` to `1.0`. [#1813] - `WebsocketContext::text` now takes an `Into`. [#1864] @@ -72,21 +75,21 @@ [#1813]: https://github.com/actix/actix-web/pull/1813 [#1864]: https://github.com/actix/actix-web/pull/1864 - ## 3.0.0 - 2020-09-11 + - No significant changes from `3.0.0-beta.2`. - ## 3.0.0-beta.2 - 2020-09-10 + - Update `actix-*` dependencies to latest versions. - ## [3.0.0-beta.1] - 2020-xx-xx + - Update `actix-web` & `actix-http` dependencies to beta.1 - Bump minimum supported Rust version to 1.40 - ## [3.0.0-alpha.1] - 2020-05-08 + - Update the actix-web dependency to 3.0.0-alpha.1 - Update the actix dependency to 0.10.0-alpha.2 - Update the actix-http dependency to 2.0.0-alpha.3 @@ -109,8 +112,7 @@ ## [1.0.2] - 2019-07-20 -- Add `ws::start_with_addr()`, returning the address of the created actor, along - with the `HttpResponse`. +- Add `ws::start_with_addr()`, returning the address of the created actor, along with the `HttpResponse`. - Add support for specifying protocols on websocket handshake #835 diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 633c3c373..e89baed96 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "4.1.0" +version = "4.2.0" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] @@ -23,7 +23,7 @@ bytes = "1" bytestring = "1" futures-core = { version = "0.3.17", default-features = false } pin-project-lite = "0.2" -tokio = { version = "1.13.1", features = ["sync"] } +tokio = { version = "1.24.2", features = ["sync"] } tokio-util = { version = "0.7", features = ["codec"] } [dev-dependencies] diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index a0578994c..dce91f503 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,15 +3,15 @@ > Actix actors support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.1.0)](https://docs.rs/actix-web-actors/4.1.0) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.2.0)](https://docs.rs/actix-web-actors/4.2.0) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.1.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.1.0) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.2.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.2.0) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-actors) -- Minimum Supported Rust Version (MSRV): 1.54 +- Minimum Supported Rust Version (MSRV): 1.59 diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index cb37bfdb0..5bc510273 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -2,43 +2,49 @@ ## 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 + - Add `#[routes]` macro to support multiple paths for one handler. [#2718] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2718]: https://github.com/actix/actix-web/pull/2718 - ## 4.0.1 - 2022-06-11 + - Fix support for guard paths in route handler macros. [#2771] - Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. [#2771]: https://github.com/actix/actix-web/pull/2771 - ## 4.0.0 - 2022-02-24 + - Version aligned with `actix-web` and will remain in sync going forward. - No significant changes since `0.5.0`. - ## 0.5.0 - 2022-02-24 + - No significant changes since `0.5.0-rc.2`. - ## 0.5.0-rc.2 - 2022-02-01 + - No significant changes since `0.5.0-rc.1`. - ## 0.5.0-rc.1 - 2022-01-04 + - Minimum supported Rust version (MSRV) is now 1.54. - ## 0.5.0-beta.6 - 2021-12-11 + - No significant changes since `0.5.0-beta.5`. - ## 0.5.0-beta.5 - 2021-10-20 + - Improve error recovery potential when macro input is invalid. [#2410] - Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] - Minimum supported Rust version (MSRV) is now 1.52. @@ -46,90 +52,90 @@ [#2410]: https://github.com/actix/actix-web/pull/2410 [#2409]: https://github.com/actix/actix-web/pull/2409 - ## 0.5.0-beta.4 - 2021-09-09 + - In routing macros, paths are now validated at compile time. [#2350] - Minimum supported Rust version (MSRV) is now 1.51. [#2350]: https://github.com/actix/actix-web/pull/2350 - ## 0.5.0-beta.3 - 2021-06-17 + - No notable changes. - ## 0.5.0-beta.2 - 2021-03-09 + - Preserve doc comments when using route macros. [#2022] - Add `name` attribute to `route` macro. [#1934] [#2022]: https://github.com/actix/actix-web/pull/2022 [#1934]: https://github.com/actix/actix-web/pull/1934 - ## 0.5.0-beta.1 - 2021-02-10 + - Use new call signature for `System::new`. - ## 0.4.0 - 2020-09-20 + - Added compile success and failure testing. [#1677] - Add `route` macro for supporting multiple HTTP methods guards. [#1674] [#1677]: https://github.com/actix/actix-web/pull/1677 [#1674]: https://github.com/actix/actix-web/pull/1674 - ## 0.3.0 - 2020-09-11 + - No significant changes from `0.3.0-beta.1`. - ## 0.3.0-beta.1 - 2020-07-14 + - Add main entry-point macro that uses re-exported runtime. [#1559] [#1559]: https://github.com/actix/actix-web/pull/1559 - ## 0.2.2 - 2020-05-23 + - Add resource middleware on actix-web-codegen [#1467] [#1467]: https://github.com/actix/actix-web/pull/1467 - ## 0.2.1 - 2020-02-25 + - Add `#[allow(missing_docs)]` attribute to generated structs [#1368] - Allow the handler function to be named as `config` [#1290] [#1368]: https://github.com/actix/actix-web/issues/1368 [#1290]: https://github.com/actix/actix-web/issues/1290 - ## 0.2.0 - 2019-12-13 + - Generate code for actix-web 2.0 - ## 0.1.3 - 2019-10-14 + - Bump up `syn` & `quote` to 1.0 - Provide better error message - ## 0.1.2 - 2019-06-04 + - Add macros for head, options, trace, connect and patch http methods - ## 0.1.1 - 2019-06-01 + - Add syn "extra-traits" feature - ## 0.1.0 - 2019-05-18 + - Release - ## 0.1.0-beta.1 - 2019-04-20 + - Gen code for actix-web 1.0.0-beta.1 - ## 0.1.0-alpha.6 - 2019-04-14 + - Gen code for actix-web 1.0.0-alpha.6 - ## 0.1.0-alpha.1 - 2019-03-28 + - Initial impl diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index da5577445..51cb0dfef 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "4.1.0" +version = "4.2.0" description = "Routing and runtime macros for Actix Web" homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 821236e47..8dd3e986e 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -3,18 +3,18 @@ > Routing and runtime macros for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.1.0)](https://docs.rs/actix-web-codegen/4.1.0) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.2.0)](https://docs.rs/actix-web-codegen/4.2.0) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/4.1.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.1.0) +[![dependency status](https://deps.rs/crate/actix-web-codegen/4.2.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.2.0) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) -- Minimum Supported Rust Version (MSRV): 1.54 +- Minimum Supported Rust Version (MSRV): 1.59 ## Compile Testing diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 4b6dc43c5..5d392be1d 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -105,7 +105,7 @@ mod route; /// ``` /// # use actix_web::HttpResponse; /// # use actix_web_codegen::route; -/// #[route("/test", method = "GET", method = "HEAD")] +/// #[route("/test", method = "GET", method = "HEAD", method = "CUSTOM")] /// async fn example() -> HttpResponse { /// HttpResponse::Ok().finish() /// } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index e5493702d..0772dbd94 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -6,11 +6,11 @@ use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path}; -macro_rules! method_type { +macro_rules! standard_method_type { ( $($variant:ident, $upper:ident, $lower:ident,)+ ) => { - #[derive(Debug, PartialEq, Eq, Hash)] + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum MethodType { $( $variant, @@ -27,7 +27,7 @@ macro_rules! method_type { fn parse(method: &str) -> Result { match method { $(stringify!($upper) => Ok(Self::$variant),)+ - _ => Err(format!("Unexpected HTTP method: `{}`", method)), + _ => Err(format!("HTTP method must be uppercase: `{}`", method)), } } @@ -41,7 +41,7 @@ macro_rules! method_type { }; } -method_type! { +standard_method_type! { Get, GET, get, Post, POST, post, Put, PUT, put, @@ -53,13 +53,6 @@ method_type! { Patch, PATCH, patch, } -impl ToTokens for MethodType { - fn to_tokens(&self, stream: &mut TokenStream2) { - let ident = Ident::new(self.as_str(), Span::call_site()); - stream.append(ident); - } -} - impl TryFrom<&syn::LitStr> for MethodType { type Error = syn::Error; @@ -69,12 +62,123 @@ impl TryFrom<&syn::LitStr> for MethodType { } } +impl ToTokens for MethodType { + fn to_tokens(&self, stream: &mut TokenStream2) { + let ident = Ident::new(self.as_str(), Span::call_site()); + stream.append(ident); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum MethodTypeExt { + Standard(MethodType), + Custom(LitStr), +} + +impl MethodTypeExt { + /// Returns a single method guard token stream. + fn to_tokens_single_guard(&self) -> TokenStream2 { + match self { + MethodTypeExt::Standard(method) => { + quote! { + .guard(::actix_web::guard::#method()) + } + } + MethodTypeExt::Custom(lit) => { + quote! { + .guard(::actix_web::guard::Method( + ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap() + )) + } + } + } + } + + /// Returns a multi-method guard chain token stream. + fn to_tokens_multi_guard(&self, or_chain: Vec) -> 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 { + match MethodType::try_from(value) { + Ok(method) => Ok(MethodTypeExt::Standard(method)), + Err(_) if value.value().chars().all(|c| c.is_ascii_uppercase()) => { + Ok(MethodTypeExt::Custom(value.clone())) + } + Err(err) => Err(err), + } + } +} + struct Args { path: syn::LitStr, resource_name: Option, guards: Vec, wrappers: Vec, - methods: HashSet, + methods: HashSet, } impl Args { @@ -99,7 +203,7 @@ impl Args { let is_route_macro = method.is_none(); if let Some(method) = method { - methods.insert(method); + methods.insert(MethodTypeExt::Standard(method)); } for arg in args { @@ -116,6 +220,7 @@ impl Args { )); } }, + NestedMeta::Meta(syn::Meta::NameValue(nv)) => { if nv.path.is_ident("name") { if let syn::Lit::Str(lit) = nv.lit { @@ -151,8 +256,7 @@ impl Args { "HTTP method forbidden here. To handle multiple methods, use `route` instead", )); } else if let syn::Lit::Str(ref lit) = nv.lit { - let method = MethodType::try_from(lit)?; - if !methods.insert(method) { + if !methods.insert(MethodTypeExt::try_from(lit)?) { return Err(syn::Error::new_spanned( &nv.lit, format!( @@ -174,11 +278,13 @@ impl Args { )); } } + arg => { return Err(syn::Error::new_spanned(arg, "Unknown attribute.")); } } } + Ok(Args { path: path.unwrap(), resource_name, @@ -299,22 +405,19 @@ impl ToTokens for Route { .map_or_else(|| name.to_string(), LitStr::value); let method_guards = { - let mut others = methods.iter(); + debug_assert!(!methods.is_empty(), "Args::methods should not be empty"); - // unwrapping since length is checked to be at least one + let mut others = methods.iter(); let first = others.next().unwrap(); if methods.len() > 1 { - quote! { - .guard( - ::actix_web::guard::Any(::actix_web::guard::#first()) - #(.or(::actix_web::guard::#others()))* - ) - } + let other_method_guards = others + .map(|method_ext| method_ext.to_tokens_multi_guard_or_chain()) + .collect(); + + first.to_tokens_multi_guard(other_method_guards) } else { - quote! { - .guard(::actix_web::guard::#first()) - } + first.to_tokens_single_guard() } }; @@ -325,7 +428,6 @@ impl ToTokens for Route { #(.guard(::actix_web::guard::fn_guard(#guards)))* #(.wrap(#wrappers))* .to(#name); - ::actix_web::dev::HttpServiceFactory::register(__resource, __config); } }) diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 10e906967..f28654cd9 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -86,7 +86,18 @@ async fn get_param_test(_: web::Path) -> impl Responder { HttpResponse::Ok() } -#[route("/multi", method = "GET", method = "POST", method = "HEAD")] +#[route("/hello", method = "HELLO")] +async fn custom_route_test() -> impl Responder { + HttpResponse::Ok() +} + +#[route( + "/multi", + method = "GET", + method = "POST", + method = "HEAD", + method = "HELLO" +)] async fn route_test() -> impl Responder { HttpResponse::Ok() } diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 26aec7d28..8839dca3d 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -9,9 +9,11 @@ fn compile_macros() { t.pass("tests/trybuild/route-ok.rs"); t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); - t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); + t.pass("tests/trybuild/route-custom-method.rs"); + t.compile_fail("tests/trybuild/route-custom-lowercase.rs"); + t.pass("tests/trybuild/routes-ok.rs"); t.compile_fail("tests/trybuild/routes-missing-method-fail.rs"); t.compile_fail("tests/trybuild/routes-missing-args-fail.rs"); diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs b/actix-web-codegen/tests/trybuild/route-custom-lowercase.rs similarity index 65% rename from actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs rename to actix-web-codegen/tests/trybuild/route-custom-lowercase.rs index 1a50e01bc..61abb5bc2 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-custom-lowercase.rs @@ -1,6 +1,8 @@ use actix_web_codegen::*; +use actix_web::http::Method; +use std::str::FromStr; -#[route("/", method="UNEXPECTED")] +#[route("/", method = "hello")] async fn index() -> String { "Hello World!".to_owned() } @@ -11,7 +13,7 @@ async fn main() { let srv = actix_test::start(|| App::new().service(index)); - let request = srv.get("/"); + let request = srv.request(Method::from_str("hello").unwrap(), srv.url("/")); let response = request.send().await.unwrap(); assert!(response.status().is_success()); } diff --git a/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr new file mode 100644 index 000000000..243c4dd68 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr @@ -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 {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 {index}` + | | + | required by a bound introduced by this call + | +note: required by a bound in `App::::service` + --> $WORKSPACE/actix-web/src/app.rs + | + | F: HttpServiceFactory + 'static, + | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/actix-web-codegen/tests/trybuild/route-custom-method.rs b/actix-web-codegen/tests/trybuild/route-custom-method.rs new file mode 100644 index 000000000..525a60b83 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-custom-method.rs @@ -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()); +} diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr deleted file mode 100644 index 3df5d9f5a..000000000 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ /dev/null @@ -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 {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 {index}` - | | - | required by a bound introduced by this call - | -note: required by a bound in `App::::service` - --> $WORKSPACE/actix-web/src/app.rs - | - | F: HttpServiceFactory + 'static, - | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 8ea60266e..1eb1f02c9 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -1,14 +1,26 @@ # Changelog ## Unreleased - 2022-xx-xx + +## 4.3.1 - 2023-02-26 + +- Add support for custom methods with the `#[route]` macro. [#2969] + +[#2969]: https://github.com/actix/actix-web/pull/2969 + +## 4.3.0 - 2023-01-21 + ### Added -- Add `ContentDisposition::attachment` constructor. [#2867] + +- Add `ContentDisposition::attachment()` constructor. [#2867] - Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784] - Add `Logger::custom_response_replace()`. [#2631] - Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961] -- Add `guard::Acceptable` for matching against `Accept` header mime types. [#2265] +- Add `guard::Acceptable` for matching against `Accept` header MIME types. [#2265] +- Add fallible versions of `test` helpers: `try_call_service()`, `try_call_and_read_body_json()`, `try_read_body()`, and `try_read_body_json()`. [#2961] ### Fixed + - Add `Allow` header to `Resource`'s default responses when no routes are matched. [#2949] [#1961]: https://github.com/actix/actix-web/pull/1961 @@ -17,40 +29,47 @@ [#2784]: https://github.com/actix/actix-web/pull/2784 [#2867]: https://github.com/actix/actix-web/pull/2867 [#2949]: https://github.com/actix/actix-web/pull/2949 - +[#2961]: https://github.com/actix/actix-web/pull/2961 ## 4.2.1 - 2022-09-12 + ### Fixed + - Bump minimum version of `actix-http` dependency to fix compatibility issue. [#2871] [#2871]: https://github.com/actix/actix-web/pull/2871 - ## 4.2.0 - 2022-09-11 + ### Added + - Add `#[routes]` macro to support multiple paths for one handler. [#2718] - Add `ServiceRequest::{parts, request}()` getter methods. [#2786] - Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752] ### Changed + - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2718]: https://github.com/actix/actix-web/pull/2718 [#2752]: https://github.com/actix/actix-web/pull/2752 [#2786]: https://github.com/actix/actix-web/pull/2786 - ## 4.1.0 - 2022-06-11 + ### Added + - Add `ServiceRequest::extract()` to make it easier to use extractors when writing middlewares. [#2647] - Add `Route::wrap()` to allow individual routes to use middleware. [#2725] - Add `ServiceConfig::default_service()`. [#2338] [#2743] - Implement `ResponseError` for `std::convert::Infallible` ### Changed + - Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. ### Fixed + - Clear connection-level data on `HttpRequest` drop. [#2742] [#2338]: https://github.com/actix/actix-web/pull/2338 @@ -59,14 +78,16 @@ [#2742]: https://github.com/actix/actix-web/pull/2742 [#2743]: https://github.com/actix/actix-web/pull/2743 - ## 4.0.1 - 2022-02-25 + ### Fixed + - Use stable version in readme example. - ## 4.0.0 - 2022-02-25 + ### Dependencies + - Updated `actix-*` to Tokio v1-based versions. [#1813] - Updated `actix-web-codegen` to `4.0.0`. - Updated `cookie` to `0.16`. [#2555] @@ -76,6 +97,7 @@ - Updated `tokio` to `1`. ### Added + - Crate Features: - `cookies`; enabled by default. [#2619] - `compress-brotli`; enabled by default. [#2618] @@ -122,6 +144,7 @@ - Connection data set through the `HttpServer::on_connect` callback is now accessible only from the new `HttpRequest::conn_data()` and `ServiceRequest::conn_data()` methods. [#2491] ### Changed + - Functions: - `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] - `guard::Not` is now generic over the type of guard it wraps. [#2552] @@ -187,6 +210,7 @@ - Minimum supported Rust version (MSRV) is now 1.54. ### Fixed + - Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] - Scope and Resource middleware can access data items set on their own layer. [#2288] - Multiple calls to `App::data()` with the same type now keeps the latest call's data. [#1906] @@ -200,11 +224,13 @@ - Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] ### Security + - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. [`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html ### Removed + - Crate Features: - `compress` feature. [#2065] - Functions: @@ -333,50 +359,58 @@ [#2663]: https://github.com/actix/actix-web/pull/2663 [871ca5e4]: https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7 -
4.0.0 Pre-Releases ## 4.0.0-rc.3 - 2022-02-08 + ### Changed + - `middleware::Condition` gained a broader compatibility; `Compat` is needed in fewer cases. [#2635] ### Added + - Implement `Responder` for `Vec`. [#2625] - Re-export `KeepAlive` in `http` mod. [#2625] [#2625]: https://github.com/actix/actix-web/pull/2625 [#2635]: https://github.com/actix/actix-web/pull/2635 - ## 4.0.0-rc.2 - 2022-02-02 + ### Added + - On-by-default `macros` feature flag to enable routing and runtime macros. [#2619] ### Removed + - `rt::{Arbiter, ArbiterHandle}` re-exports. [#2619] [#2619]: https://github.com/actix/actix-web/pull/2619 - ## 4.0.0-rc.1 - 2022-01-31 + ### Changed + - Rename `HttpServer::{client_timeout => client_request_timeout}`. [#2611] - Rename `HttpServer::{client_shutdown => client_disconnect_timeout}`. [#2611] ### Removed + - `impl Future for HttpResponse`. [#2601] [#2601]: https://github.com/actix/actix-web/pull/2601 [#2611]: https://github.com/actix/actix-web/pull/2611 - ## 4.0.0-beta.21 - 2022-01-21 + ### Added + - `HttpResponse::add_removal_cookie`. [#2586] - `Logger::log_target`. [#2594] ### Removed + - `HttpRequest::req_data[_mut]()`; request-local data is still available through `.extensions()`. [#2585] - `HttpRequestBuilder::del_cookie`. [#2591] @@ -385,13 +419,15 @@ [#2591]: https://github.com/actix/actix-web/pull/2591 [#2594]: https://github.com/actix/actix-web/pull/2594 - ## 4.0.0-beta.20 - 2022-01-14 + ### Added + - `GuardContext::header` [#2569] - `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988] ### Changed + - `HttpResponse` can now be used as a `Responder` with any body type. [#2567] - `Result` extractor wrapper can now convert error types. [#2581] - Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581] @@ -405,52 +441,61 @@ [#2582]: https://github.com/actix/actix-web/pull/2582 [#2584]: https://github.com/actix/actix-web/pull/2584 - ## 4.0.0-beta.19 - 2022-01-04 + ### Added + - `impl Hash` for `http::header::Encoding`. [#2501] - `AcceptEncoding::negotiate()`. [#2501] ### Changed + - `AcceptEncoding::preference` now returns `Option>`. [#2501] - Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501] - `http::header::Encoding` now only represents `Content-Encoding` types. [#2501] ### Fixed + - Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] ### Removed + - `Compress::new`; restricting compression algorithm is done through feature flags. [#2501] - `BodyEncoding` trait; signalling content encoding is now only done via the `Content-Encoding` header. [#2565] [#2501]: https://github.com/actix/actix-web/pull/2501 [#2565]: https://github.com/actix/actix-web/pull/2565 - ## 4.0.0-beta.18 - 2021-12-29 + ### Changed + - Update `cookie` dependency (re-exported) to `0.16`. [#2555] - Minimum supported Rust version (MSRV) is now 1.54. ### Security + - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. [#2555]: https://github.com/actix/actix-web/pull/2555 -[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html - +[`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html ## 4.0.0-beta.17 - 2021-12-29 + ### Added + - `guard::GuardContext` for use with the `Guard` trait. [#2552] - `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552] ### Changed + - `Guard` trait now receives a `&GuardContext`. [#2552] - `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] - Some guards now return `impl Guard` and their concrete types are made private: `guard::Header` and all the method guards. [#2552] - The `Not` guard is now generic over the type of guard it wraps. [#2552] ### Fixed + - Rename `ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554] - `ConnectionInfo::peer_addr` will not return the port number. [#2554] - `ConnectionInfo::realip_remote_addr` will not return the port number if sourcing the IP from the peer's socket address. [#2554] @@ -458,22 +503,25 @@ [#2552]: https://github.com/actix/actix-web/pull/2552 [#2554]: https://github.com/actix/actix-web/pull/2554 - ## 4.0.0-beta.16 - 2021-12-27 + ### Changed + - No longer require `Scope` service body type to be boxed. [#2523] - No longer require `Resource` service body type to be boxed. [#2526] [#2523]: https://github.com/actix/actix-web/pull/2523 [#2526]: https://github.com/actix/actix-web/pull/2526 - ## 4.0.0-beta.15 - 2021-12-17 + ### Added + - Method on `Responder` trait (`customize`) for customizing responders and `CustomizeResponder` struct. [#2510] - Implement `Debug` for `DefaultHeaders`. [#2510] ### Changed + - Align `DefaultHeader` method terminology, deprecating previous methods. [#2510] - Response service types in `ErrorHandlers` middleware now use `ServiceResponse>` to allow changing the body type. [#2515] - Both variants in `ErrorHandlerResponse` now use `ServiceResponse>`. [#2515] @@ -483,6 +531,7 @@ - Relax body type and error bounds on test utilities. [#2518] ### Removed + - Top-level `EitherExtractError` export. [#2510] - Conversion implementations for `either` crate. [#2516] - `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518] @@ -492,9 +541,10 @@ [#2516]: https://github.com/actix/actix-web/pull/2516 [#2518]: https://github.com/actix/actix-web/pull/2518 - ## 4.0.0-beta.14 - 2021-12-11 + ### Added + - Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] - `AcceptEncoding` typed header. [#2482] - `Range` typed header. [#2485] @@ -505,6 +555,7 @@ - `ServiceResponse::into_parts`. [#2499] ### Changed + - Rename `Accept::{mime_precedence => ranked}`. [#2480] - Rename `Accept::{mime_preference => preference}`. [#2480] - Un-deprecate `App::data_factory`. [#2484] @@ -514,11 +565,13 @@ - Request-local data container is no longer part of a `RequestHead`. Instead it is a distinct part of a `Request`. [#2487] ### Fixed + - Accept wildcard `*` items in `AcceptLanguage`. [#2480] - Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468] - Typed headers containing lists that require one or more items now enforce this minimum. [#2482] ### Removed + - `ConnectionInfo::get`. [#2487] [#2430]: https://github.com/actix/actix-web/pull/2430 @@ -533,46 +586,54 @@ [#2493]: https://github.com/actix/actix-web/pull/2493 [#2499]: https://github.com/actix/actix-web/pull/2499 - ## 4.0.0-beta.13 - 2021-11-30 + ### Changed + - Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2474]: https://github.com/actix/actix-web/pull/2474 - ## 4.0.0-beta.12 - 2021-11-22 + ### Changed + - Compress middleware's response type is now `AnyBody>`. [#2448] ### Fixed + - Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] ### Removed + - `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446] [#2446]: https://github.com/actix/actix-web/pull/2446 [#2448]: https://github.com/actix/actix-web/pull/2448 - ## 4.0.0-beta.11 - 2021-11-15 + ### Added + - Re-export `dev::ServerHandle` from `actix-server`. [#2442] ### Changed + - `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] - Update `actix-server` to `2.0.0-beta.9`. [#2442] [#2423]: https://github.com/actix/actix-web/pull/2423 [#2442]: https://github.com/actix/actix-web/pull/2442 - ## 4.0.0-beta.10 - 2021-10-20 + ### Added + - Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] - `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] ### Changed + - Associated type `FromRequest::Config` was removed. [#2233] - Inner field made private on `web::Payload`. [#2384] - `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403] @@ -580,6 +641,7 @@ - Minimum supported Rust version (MSRV) is now 1.52. ### Removed + - Useless `ServiceResponse::checked_expr` method. [#2401] [#2233]: https://github.com/actix/actix-web/pull/2233 @@ -590,18 +652,21 @@ [#2409]: https://github.com/actix/actix-web/pull/2409 [#2414]: https://github.com/actix/actix-web/pull/2414 - ## 4.0.0-beta.9 - 2021-09-09 + ### Added + - Re-export actix-service `ServiceFactory` in `dev` module. [#2325] ### Changed + - Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] - Move `BaseHttpResponse` to `dev::Response`. [#2379] - Enable `TestRequest::param` to accept more than just static strings. [#2172] - Minimum supported Rust version (MSRV) is now 1.51. ### Fixed + - Fix quality parse error in Accept-Encoding header. [#2344] - Re-export correct type at `web::HttpResponse`. [#2379] @@ -610,20 +675,23 @@ [#2344]: https://github.com/actix/actix-web/pull/2344 [#2379]: https://github.com/actix/actix-web/pull/2379 - ## 4.0.0-beta.8 - 2021-06-26 + ### Added + - Add `ServiceRequest::parts_mut`. [#2177] - Add extractors for `Uri` and `Method`. [#2263] - Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263] - Add `Route::service` for using hand-written services as handlers. [#2262] ### Changed + - Change compression algorithm features flags. [#2250] - Deprecate `App::data` and `App::data_factory`. [#2271] - Smarter extraction of `ConnectionInfo` parts. [#2282] ### Fixed + - Scope and Resource middleware can access data items set on their own layer. [#2288] [#2177]: https://github.com/actix/actix-web/pull/2177 @@ -634,12 +702,14 @@ [#2282]: https://github.com/actix/actix-web/pull/2282 [#2288]: https://github.com/actix/actix-web/pull/2288 - ## 4.0.0-beta.7 - 2021-06-17 + ### Added + - `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] ### Changed + - Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162] - `ServiceResponse::error_response` now uses body type of `Body`. [#2201] - `ServiceResponse::checked_expr` now returns a `Result`. [#2201] @@ -652,6 +722,7 @@ - `middleware::normalize` now will not try to normalize URIs with no valid path [#2246] ### Removed + - `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] [#2162]: https://github.com/actix/actix-web/pull/2162 @@ -660,36 +731,39 @@ [#2253]: https://github.com/actix/actix-web/pull/2253 [#2246]: https://github.com/actix/actix-web/pull/2246 - ## 4.0.0-beta.6 - 2021-04-17 + ### Added + - `HttpResponse` and `HttpResponseBuilder` types. [#2065] ### Changed + - Most error types are now marked `#[non_exhaustive]`. [#2148] - Methods on `ContentDisposition` that took `T: AsRef` now take `impl AsRef`. [#2065]: https://github.com/actix/actix-web/pull/2065 [#2148]: https://github.com/actix/actix-web/pull/2148 - ## 4.0.0-beta.5 - 2021-04-02 + ### Added + - `Header` extractor for extracting common HTTP headers in handlers. [#2094] - Added `TestServer::client_headers` method. [#2097] ### Changed -- `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed - instead of skipping. (Only the first error is kept when multiple error occur) [#2093] + +- `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed instead of skipping. (Only the first error is kept when multiple error occur) [#2093] ### Fixed + - Double ampersand in Logger format is escaped correctly. [#2067] ### Removed -- The `client` mod was removed. Clients should now use `awc` directly. - [871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7) -- Integration testing was moved to new `actix-test` crate. Namely these items from the `test` - module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112] + +- The `client` mod was removed. Clients should now use `awc` directly. [871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7) +- Integration testing was moved to new `actix-test` crate. Namely these items from the `test` module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112] [#2067]: https://github.com/actix/actix-web/pull/2067 [#2093]: https://github.com/actix/actix-web/pull/2093 @@ -697,50 +771,48 @@ [#2097]: https://github.com/actix/actix-web/pull/2097 [#2112]: https://github.com/actix/actix-web/pull/2112 - ## 4.0.0-beta.4 - 2021-03-09 + ### Changed + - Feature `cookies` is now optional and enabled by default. [#1981] -- `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default - behaviour of the `web::Json` extractor. [#2010] +- `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default behaviour of the `web::Json` extractor. [#2010] [#1981]: https://github.com/actix/actix-web/pull/1981 [#2010]: https://github.com/actix/actix-web/pull/2010 - ## 4.0.0-beta.3 - 2021-02-10 + - Update `actix-web-codegen` to `0.5.0-beta.1`. - ## 4.0.0-beta.2 - 2021-02-10 + ### Added -- The method `Either, web::Form>::into_inner()` which returns the inner type for - whichever variant was created. Also works for `Either, web::Json>`. [#1894] + +- The method `Either, web::Form>::into_inner()` which returns the inner type for whichever variant was created. Also works for `Either, web::Json>`. [#1894] - Add `services!` macro for helping register multiple services to `App`. [#1933] - Enable registering a vec of services of the same type to `App` [#1933] ### Changed -- Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. - Making it simpler and more performant. [#1891] + +- Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. Making it simpler and more performant. [#1891] - `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893] - `ServiceRequest::from_request` can no longer fail. [#1893] - Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894] -- `test::{call_service, read_response, read_response_json, send_request}` take `&Service` - in argument [#1905] -- `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure - argument. [#1905] +- `test::{call_service, read_response, read_response_json, send_request}` take `&Service` in argument [#1905] +- `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure argument. [#1905] - `web::block` no longer requires the output is a Result. [#1957] ### Fixed + - Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906] ### Removed + - Public field of `web::Path` has been made private. [#1894] - Public field of `web::Query` has been made private. [#1894] - `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869] -- `AppService::set_service_data`; for custom HTTP service factories adding application data, use the - layered data model by calling `ServiceRequest::add_data_container` when handling - requests instead. [#1906] +- `AppService::set_service_data`; for custom HTTP service factories adding application data, use the layered data model by calling `ServiceRequest::add_data_container` when handling requests instead. [#1906] [#1891]: https://github.com/actix/actix-web/pull/1891 [#1893]: https://github.com/actix/actix-web/pull/1893 @@ -751,30 +823,30 @@ [#1933]: https://github.com/actix/actix-web/pull/1933 [#1957]: https://github.com/actix/actix-web/pull/1957 - ## 4.0.0-beta.1 - 2021-01-07 + ### Added -- `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and - `Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865] + +- `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and `Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865] ### Changed + - Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] - Bumped `rand` to `0.8`. - Update `rust-tls` to `0.19`. [#1813] - Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852] -- The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration - guide for implications. [#1875] +- The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration guide for implications. [#1875] - Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875] - MSRV is now 1.46.0. ### Fixed + - Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] ### Removed -- Public modules `middleware::{normalize, err_handlers}`. All necessary middleware types are now - exposed directly by the `middleware` module. -- Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported - from `actix_web::error` module. [#1878] + +- Public modules `middleware::{normalize, err_handlers}`. All necessary middleware types are now exposed directly by the `middleware` module. +- Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported from `actix_web::error` module. [#1878] [#1812]: https://github.com/actix/actix-web/pull/1812 [#1813]: https://github.com/actix/actix-web/pull/1813 @@ -786,14 +858,17 @@
## 3.3.3 - 2021-12-18 + ### Changed + - Soft-deprecate `NormalizePath::default()`, noting upcoming behavior change in v4. [#2529] [#2529]: https://github.com/actix/actix-web/pull/2529 - ## 3.3.2 - 2020-12-01 + ### Fixed + - Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762] - Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798] - Increase minimum `socket2` version. [#1803] @@ -802,24 +877,27 @@ [#1798]: https://github.com/actix/actix-web/pull/1798 [#1803]: https://github.com/actix/actix-web/pull/1803 - ## 3.3.1 - 2020-11-29 + - Ensure `actix-http` dependency uses same `serde_urlencoded`. - ## 3.3.0 - 2020-11-25 + ### Added + - Add `Either` extractor helper. [#1788] ### Changed + - Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 [#1788]: https://github.com/actix/actix-web/pull/1788 - ## 3.2.0 - 2020-10-30 + ### Added + - Implement `exclude_regex` for Logger middleware. [#1723] - Add request-local data extractor `web::ReqData`. [#1748] - Add ability to register closure for request middleware logging. [#1749] @@ -827,6 +905,7 @@ - Expose `on_connect` for access to the connection stream before request is handled. [#1754] ### Changed + - Updated actix-web-codegen dependency for access to new `#[route(...)]` multi-method macro. - Print non-configured `Data` type when attempting extraction. [#1743] - Re-export bytes::Buf{Mut} in web module. [#1750] @@ -839,52 +918,53 @@ [#1754]: https://github.com/actix/actix-web/pull/1754 [#1749]: https://github.com/actix/actix-web/pull/1749 - ## 3.1.0 - 2020-09-29 + ### Changed -- Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` - to retain any trailing slashes. [#1695] -- Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` - via `web::Data::from` [#1710] + +- Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` to retain any trailing slashes. [#1695] +- Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` via `web::Data::from` [#1710] ### Fixed + - `ResourceMap` debug printing is no longer infinitely recursive. [#1708] [#1695]: https://github.com/actix/actix-web/pull/1695 [#1708]: https://github.com/actix/actix-web/pull/1708 [#1710]: https://github.com/actix/actix-web/pull/1710 - ## 3.0.2 - 2020-09-15 + ### Fixed + - `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678] [#1678]: https://github.com/actix/actix-web/pull/1678 - ## 3.0.1 - 2020-09-13 + ### Changed + - `middleware::normalize::TrailingSlash` enum is now accessible. [#1673] [#1673]: https://github.com/actix/actix-web/pull/1673 - ## 3.0.0 - 2020-09-11 + - No significant changes from `3.0.0-beta.4`. - ## 3.0.0-beta.4 - 2020-09-09 + ### Added -- `middleware::NormalizePath` now has configurable behavior for either always having a trailing - slash, or as the new addition, always trimming trailing slashes. [#1639] + +- `middleware::NormalizePath` now has configurable behavior for either always having a trailing slash, or as the new addition, always trimming trailing slashes. [#1639] ### Changed + - Update actix-codec and actix-utils dependencies. [#1634] -- `FormConfig` and `JsonConfig` configurations are now also considered when set - using `App::data`. [#1641] +- `FormConfig` and `JsonConfig` configurations are now also considered when set using `App::data`. [#1641] - `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655] -- `HttpServer::maxconnrate` is renamed to the more expressive - `HttpServer::max_connection_rate`. [#1655] +- `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. [#1655] [#1639]: https://github.com/actix/actix-web/pull/1639 [#1641]: https://github.com/actix/actix-web/pull/1641 @@ -892,22 +972,23 @@ [#1655]: https://github.com/actix/actix-web/pull/1655 ## 3.0.0-beta.3 - 2020-08-17 + ### Changed + - Update `rustls` to 0.18 - ## 3.0.0-beta.2 - 2020-08-17 + ### Changed -- `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set - using `App::data`. [#1610] -- `web::Path` now has a public representation: `web::Path(pub T)` that enables - destructuring. [#1594] -- `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to - access `HttpRequest` which already allows this. [#1618] + +- `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set using `App::data`. [#1610] +- `web::Path` now has a public representation: `web::Path(pub T)` that enables destructuring. [#1594] +- `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to access `HttpRequest` which already allows this. [#1618] - Re-export all error types from `awc`. [#1621] - MSRV is now 1.42.0. ### Fixed + - Memory leak of app data in pooled requests. [#1609] [#1594]: https://github.com/actix/actix-web/pull/1594 @@ -916,28 +997,32 @@ [#1618]: https://github.com/actix/actix-web/pull/1618 [#1621]: https://github.com/actix/actix-web/pull/1621 - ## 3.0.0-beta.1 - 2020-07-13 + ### Added + - Re-export `actix_rt::main` as `actix_web::main`. -- `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched - resource pattern. +- `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched resource pattern. - `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name. ### Changed + - Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] - Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency. - MSRV is now 1.41.1 ### Fixed + - `NormalizePath` improved consistency when path needs slashes added _and_ removed. - ## 3.0.0-alpha.3 - 2020-05-21 + ### Added + - Add option to create `Data` from `Arc` [#1509] ### Changed + - Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486] - Fix audit issue logging by default peer address [#1485] - Bump minimum supported Rust version to 1.40 @@ -960,7 +1045,6 @@ [#1452]: https://github.com/actix/actix-web/pull/1452 [#1486]: https://github.com/actix/actix-web/pull/1486 - ## [3.0.0-alpha.1] - 2020-03-11 ### Added @@ -1006,7 +1090,6 @@ - Fix `AppConfig::secure()` is always false. #1202 - ## [2.0.0-alpha.6] - 2019-12-15 ### Fixed @@ -1031,7 +1114,6 @@ - Migrate to tokio 0.2 - ## [2.0.0-alpha.1] - 2019-11-22 ### Changed @@ -1040,7 +1122,6 @@ - Remove implementation of `Responder` for `()`. (#1167) - ## [1.0.9] - 2019-11-14 ### Added @@ -1051,20 +1132,17 @@ - Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129) - ## [1.0.8] - 2019-09-25 ### Added -- Add `Scope::register_data` and `Resource::register_data` methods, parallel to - `App::register_data`. +- Add `Scope::register_data` and `Resource::register_data` methods, parallel to `App::register_data`. - Add `middleware::Condition` that conditionally enables another middleware - Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload` -- Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path, - which is useful for example with systemd. +- Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path, which is useful for example with systemd. ### Changed @@ -1072,14 +1150,12 @@ - Use actix-testing for testing utils - ## [1.0.7] - 2019-08-29 ### Fixed - Request Extensions leak #1062 - ## [1.0.6] - 2019-08-28 ### Added @@ -1090,8 +1166,7 @@ - Add `into_inner` to `Data` -- Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set - the header in test requests. +- Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set the header in test requests. ### Changed @@ -1103,35 +1178,30 @@ - Update url to 2.1 - ## [1.0.5] - 2019-07-18 ### Added - Unix domain sockets (HttpServer::bind_uds) #92 -- Actix now logs errors resulting in "internal server error" responses always, with the `error` - logging level +- Actix now logs errors resulting in "internal server error" responses always, with the `error` logging level ### Fixed - Restored logging of errors through the `Logger` middleware - ## [1.0.4] - 2019-07-17 ### Added - Add `Responder` impl for `(T, StatusCode) where T: Responder` -- Allow to access app's resource map via - `ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods. +- Allow to access app's resource map via `ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods. ### Changed - Upgrade `rand` dependency version to 0.7 - ## [1.0.3] - 2019-06-28 ### Added @@ -1140,8 +1210,7 @@ ### Changed -- Use `encoding_rs` crate instead of unmaintained `encoding` crate - +- Use `encoding_rs` crate instead of unmaintained `encoding` crate ## [1.0.2] - 2019-06-17 @@ -1151,7 +1220,6 @@ - Move identity middleware to `actix-identity` crate. - ## [1.0.1] - 2019-06-17 ### Added @@ -1176,7 +1244,6 @@ - HttpRequest::url_for is broken with nested scopes #915 - ## [1.0.0] - 2019-06-05 ### Added @@ -1185,8 +1252,7 @@ - Add `ServiceRequest::set_payload()` method. -- Add `test::TestRequest::set_json()` convenience method to automatically - serialize data and set header in test requests. +- Add `test::TestRequest::set_json()` convenience method to automatically serialize data and set header in test requests. - Add macros for head, options, trace, connect and patch http methods @@ -1200,7 +1266,6 @@ - Clear http requests pool on app service drop #860 - ## [1.0.0-rc] - 2019-05-18 ### Added @@ -1216,7 +1281,6 @@ - Codegen with parameters in the path only resolves the first registered endpoint #841 - ## [1.0.0-beta.4] - 2019-05-12 ### Added @@ -1228,7 +1292,6 @@ - `App::configure` take an `FnOnce` instead of `Fn` - Upgrade actix-net crates - ## [1.0.0-beta.3] - 2019-05-04 ### Added @@ -1237,11 +1300,9 @@ ### Changed -- Extractor configuration could be registered with `App::data()` - or with `Resource::data()` #775 +- Extractor configuration could be registered with `App::data()` or with `Resource::data()` #775 -- Route data is unified with app data, `Route::data()` moved to resource - level to `Resource::data()` +- Route data is unified with app data, `Route::data()` moved to resource level to `Resource::data()` - CORS handling without headers #702 @@ -1255,7 +1316,6 @@ - `App::data_factory()` is deleted. - ## [1.0.0-beta.2] - 2019-04-24 ### Added @@ -1264,7 +1324,7 @@ - Add helper functions for reading response body `test::read_body()` -- Add support for `remainder match` (i.e "/path/{tail}*") +- Add support for `remainder match` (i.e "/path/{tail}\*") - Extend `Responder` trait, allow to override status code and headers. @@ -1278,13 +1338,11 @@ - Fix async web::Data factory handling - ## [1.0.0-beta.1] - 2019-04-20 ### Added -- Add helper functions for reading test response body, - `test::read_response()` and test::read_response_json()` +- Add helper functions for reading test response body, `test::read_response()` and test::read_response_json()` - Add `.peer_addr()` #744 @@ -1304,7 +1362,6 @@ - Fixed `TestRequest::app_data()` - ## [1.0.0-alpha.6] - 2019-04-14 ### Changed @@ -1313,12 +1370,10 @@ - Remove generic type for request payload, always use default. -- Removed `Decompress` middleware. Bytes, String, Json, Form extractors - automatically decompress payload. +- Removed `Decompress` middleware. Bytes, String, Json, Form extractors automatically decompress payload. - Make extractor config type explicit. Add `FromRequest::Config` associated type. - ## [1.0.0-alpha.5] - 2019-04-12 ### Added @@ -1329,7 +1384,6 @@ - Removed native-tls support - ## [1.0.0-alpha.4] - 2019-04-08 ### Added @@ -1352,7 +1406,6 @@ - Fix body propagation in Response::from_error. #760 - ## [1.0.0-alpha.3] - 2019-04-02 ### Changed @@ -1367,7 +1420,6 @@ - Removed unused `actix_web::web::md()` - ## [1.0.0-alpha.2] - 2019-03-29 ### Added diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index 6078d5739..6eb1f862b 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.2.1" +version = "4.3.1" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -68,11 +68,11 @@ actix-service = "2" actix-utils = "3" 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-web-codegen = { version = "4.1", optional = true } +actix-web-codegen = { version = "4.2", optional = true } -ahash = "0.7" +ahash = "0.8" bytes = "1" bytestring = "1" cfg-if = "1" @@ -103,7 +103,7 @@ actix-test = { version = "0.1", features = ["openssl", "rustls"] } awc = { version = "3", features = ["openssl"] } brotli = "3.3.3" -const-str = "0.4" +const-str = "0.3" criterion = { version = "0.4", features = ["html_reports"] } env_logger = "0.9" flate2 = "1.0.13" @@ -115,7 +115,7 @@ serde = { version = "1.0", features = ["derive"] } static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } zstd = "0.12" [[test]] diff --git a/actix-web/MIGRATION-0.x.md b/actix-web/MIGRATION-0.x.md index 1b60c36d1..6af514642 100644 --- a/actix-web/MIGRATION-0.x.md +++ b/actix-web/MIGRATION-0.x.md @@ -1,7 +1,6 @@ # 0.7.15 -- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in - your routes, you should use `%20`. +- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in your routes, you should use `%20`. instead of @@ -29,13 +28,11 @@ fn main() { # 0.7.4 -- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple - even for handler with one parameter. +- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple even for handler with one parameter. # 0.7 -- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload - use `HttpMessage::payload()` method. +- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload use `HttpMessage::payload()` method. instead of @@ -60,8 +57,7 @@ fn index(req: HttpRequest) -> impl Responder { } ``` -- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) - trait uses `&HttpRequest` instead of `&mut HttpRequest`. +- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) trait uses `&HttpRequest` instead of `&mut HttpRequest`. - Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead. @@ -81,14 +77,11 @@ fn index((query, json): (Query<..>, Json impl Responder {} - `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value -- Removed deprecated `HttpServer::threads()`, use - [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead. +- Removed deprecated `HttpServer::threads()`, use [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead. -- Renamed `client::ClientConnectorError::Connector` to - `client::ClientConnectorError::Resolver` +- Renamed `client::ClientConnectorError::Connector` to `client::ClientConnectorError::Resolver` -- `Route::with()` does not return `ExtractorConfig`, to configure - extractor use `Route::with_config()` +- `Route::with()` does not return `ExtractorConfig`, to configure extractor use `Route::with_config()` instead of @@ -116,23 +109,19 @@ fn main() { } ``` -- `Route::with_async()` does not return `ExtractorConfig`, to configure - extractor use `Route::with_async_config()` +- `Route::with_async()` does not return `ExtractorConfig`, to configure extractor use `Route::with_async_config()` # 0.6 - `Path` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest` -- `ws::Message::Close` now includes optional close reason. - `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed. +- `ws::Message::Close` now includes optional close reason. `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed. - `HttpServer::threads()` renamed to `HttpServer::workers()`. -- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. - Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead. +- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead. -- `HttpRequest::extensions()` returns read only reference to the request's Extension - `HttpRequest::extensions_mut()` returns mutable reference. +- `HttpRequest::extensions()` returns read only reference to the request's Extension `HttpRequest::extensions_mut()` returns mutable reference. - Instead of @@ -146,8 +135,7 @@ fn main() { - `FromRequest::Result` has to implement `Into>` -- [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) - is generic over `S` +- [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) is generic over `S` - Use `Query` extractor instead of HttpRequest::query()`. @@ -163,23 +151,19 @@ or let q = Query::>::extract(req); ``` -- Websocket operations are implemented as `WsWriter` trait. - you need to use `use actix_web::ws::WsWriter` +- Websocket operations are implemented as `WsWriter` trait. you need to use `use actix_web::ws::WsWriter` # 0.5 -- `HttpResponseBuilder::body()`, `.finish()`, `.json()` - methods return `HttpResponse` instead of `Result` +- `HttpResponseBuilder::body()`, `.finish()`, `.json()` methods return `HttpResponse` instead of `Result` -- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` - moved to `actix_web::http` module +- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` moved to `actix_web::http` module - `actix_web::header` moved to `actix_web::http::header` - `NormalizePath` moved to `actix_web::http` module -- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, - shortcut for `actix_web::server::HttpServer::new()` +- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, shortcut for `actix_web::server::HttpServer::new()` - `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself @@ -187,11 +171,9 @@ let q = Query::>::extract(req); - `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type -- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` - functions should be used instead +- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` functions should be used instead -- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` - instead of `Result<_, http::Error>` +- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` instead of `Result<_, http::Error>` - `Application` renamed to a `App` diff --git a/actix-web/MIGRATION-1.0.md b/actix-web/MIGRATION-1.0.md index 94c6321ac..d72bb63b6 100644 --- a/actix-web/MIGRATION-1.0.md +++ b/actix-web/MIGRATION-1.0.md @@ -88,8 +88,7 @@ ) ``` -- Resource registration. 1.0 version uses generalized resource - registration via `.service()` method. +- Resource registration. 1.0 version uses generalized resource registration via `.service()` method. instead of @@ -97,9 +96,7 @@ App.new().resource("/welcome", |r| r.f(welcome)) ``` - use App's or Scope's `.service()` method. `.service()` method accepts - object that implements `HttpServiceFactory` trait. By default - actix-web provides `Resource` and `Scope` services. + use App's or Scope's `.service()` method. `.service()` method accepts object that implements `HttpServiceFactory` trait. By default actix-web provides `Resource` and `Scope` services. ```rust App.new().service( @@ -164,9 +161,7 @@ } ``` -- `.f()`, `.a()` and `.h()` handler registration methods have been removed. - Use `.to()` for handlers and `.to_async()` for async handlers. Handler function - must use extractors. +- `.f()`, `.a()` and `.h()` handler registration methods have been removed. Use `.to()` for handlers and `.to_async()` for async handlers. Handler function must use extractors. instead of @@ -210,9 +205,7 @@ } ``` -- `State` is now `Data`. You register Data during the App initialization process - and then access it from handlers either using a Data extractor or using - HttpRequest's api. +- `State` is now `Data`. You register Data during the App initialization process and then access it from handlers either using a Data extractor or using HttpRequest's api. instead of @@ -277,8 +270,7 @@ .route("/index.html", web::get().to(index)); ``` -- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` - method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead. +- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead. instead of @@ -317,8 +309,7 @@ use `use actix_multipart::Multipart` -- Response compression is not enabled by default. - To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`. +- Response compression is not enabled by default. To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`. - Session middleware moved to actix-session crate diff --git a/actix-web/MIGRATION-2.0.md b/actix-web/MIGRATION-2.0.md index 0455062d1..41e6104b9 100644 --- a/actix-web/MIGRATION-2.0.md +++ b/actix-web/MIGRATION-2.0.md @@ -1,21 +1,16 @@ # Migrating to 2.0.0 -- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to - `.await` on `run` method result, in that case it awaits server exit. +- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to `.await` on `run` method result, in that case it awaits server exit. -- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`. - Stored data is available via `HttpRequest::app_data()` method at runtime. +- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`. Stored data is available via `HttpRequest::app_data()` method at runtime. - Extractor configuration must be registered with `App::app_data()` instead of `App::data()` -- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()` - replace `fn` with `async fn` to convert sync handler to async +- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()` replace `fn` with `async fn` to convert sync handler to async -- `actix_http_test::TestServer` moved to `actix_web::test` module. To start - test server use `test::start()` or `test_start_with_config()` methods +- `actix_http_test::TestServer` moved to `actix_web::test` module. To start test server use `test::start()` or `test_start_with_config()` methods -- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders - http response. +- `ResponseError` trait has been refactored. `ResponseError::error_response()` renders http response. - Feature `rust-tls` renamed to `rustls` diff --git a/actix-web/MIGRATION-3.0.md b/actix-web/MIGRATION-3.0.md index 54bcd58bd..89255e434 100644 --- a/actix-web/MIGRATION-3.0.md +++ b/actix-web/MIGRATION-3.0.md @@ -1,31 +1,23 @@ # Migrating to 3.0.0 -- The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to - simply a `T`. To access a `Data` use `ServiceRequest::app_data::>()`. +- The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to simply a `T`. To access a `Data` use `ServiceRequest::app_data::>()`. - Cookie handling has been offloaded to the `cookie` crate: - `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs. - Some types now require lifetime parameters. -- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects - any `actix-web` method previously expecting a time v0.1 input. +- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects any `actix-web` method previously expecting a time v0.1 input. -- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now - result in `SameSite=None` being sent with the response Set-Cookie header. - To create a cookie without a SameSite attribute, remove any calls setting same_site. +- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now result in `SameSite=None` being sent with the response Set-Cookie header. To create a cookie without a SameSite attribute, remove any calls setting same_site. -- actix-http support for Actors messages was moved to actix-http crate and is enabled - with feature `actors` +- actix-http support for Actors messages was moved to actix-http crate and is enabled with feature `actors` -- content_length function is removed from actix-http. - You can set Content-Length by normally setting the response body or calling no_chunking function. +- content_length function is removed from actix-http. You can set Content-Length by normally setting the response body or calling no_chunking function. -- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a - `u64` instead of a `usize`. +- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`. -- Code that was using `path.` to access a `web::Path<(A, B, C)>`s elements now needs to use - destructuring or `.into_inner()`. For example: +- Code that was using `path.` to access a `web::Path<(A, B, C)>`s elements now needs to use destructuring or `.into_inner()`. For example: ```rust // Previously: @@ -44,9 +36,7 @@ } ``` -- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. - It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, - or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`. +- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`. - `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. diff --git a/actix-web/MIGRATION-4.0.md b/actix-web/MIGRATION-4.0.md index fbeae0680..7b1cfc03b 100644 --- a/actix-web/MIGRATION-4.0.md +++ b/actix-web/MIGRATION-4.0.md @@ -31,7 +31,7 @@ Headings marked with :warning: are **breaking behavioral changes**. They will pr - [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously) - [`#[actix_web::main]` and `#[tokio::main]`](#actix_webmain-and-tokiomain) - [`web::block`](#webblock) -- +- ## MSRV diff --git a/actix-web/README.md b/actix-web/README.md index 65076e0b8..3c6524d36 100644 --- a/actix-web/README.md +++ b/actix-web/README.md @@ -5,16 +5,7 @@

-[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.2.1)](https://docs.rs/actix-web/4.2.1) -![MSRV](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) -![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.2.1/status.svg)](https://deps.rs/crate/actix-web/4.2.1) -
-[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) -![downloads](https://img.shields.io/crates/d/actix-web.svg) -[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) +[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.3.1)](https://docs.rs/actix-web/4.3.1) ![MSRV](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) [![Dependency Status](https://deps.rs/crate/actix-web/4.3.1/status.svg)](https://deps.rs/crate/actix-web/4.3.1)
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) ![downloads](https://img.shields.io/crates/d/actix-web.svg) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

diff --git a/actix-web/examples/uds.rs b/actix-web/examples/uds.rs index ba4b25a29..15e28ba1d 100644 --- a/actix-web/examples/uds.rs +++ b/actix-web/examples/uds.rs @@ -41,7 +41,7 @@ async fn main() -> std::io::Result<()> { ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) }) - .bind_uds("/Users/fafhrd91/uds-test")? + .bind_uds("/Users/me/uds-test")? .workers(1) .run() .await diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index 0b5ba2ab6..0fc856203 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -21,7 +21,7 @@ use crate::{ Error, HttpResponse, }; -/// Service factory to convert `Request` to a `ServiceRequest`. +/// Service factory to convert [`Request`] to a [`ServiceRequest`]. /// /// It also executes data factories. pub struct AppInit @@ -155,7 +155,7 @@ where app_state: Rc, } -/// A collection of [`AppInitService`] state that shared across `HttpRequest`s. +/// A collection of state for [`AppInitService`] that is shared across [`HttpRequest`]s. pub(crate) struct AppInitServiceState { rmap: Rc, config: AppConfig, @@ -163,6 +163,7 @@ pub(crate) struct AppInitServiceState { } impl AppInitServiceState { + /// Constructs state collection from resource map and app config. pub(crate) fn new(rmap: Rc, config: AppConfig) -> Rc { Rc::new(AppInitServiceState { rmap, @@ -171,16 +172,19 @@ impl AppInitServiceState { }) } + /// Returns a reference to the application's resource map. #[inline] pub(crate) fn rmap(&self) -> &ResourceMap { &self.rmap } + /// Returns a reference to the application's configuration. #[inline] pub(crate) fn config(&self) -> &AppConfig { &self.config } + /// Returns a reference to the application's request pool. #[inline] pub(crate) fn pool(&self) -> &HttpRequestPool { &self.pool diff --git a/actix-web/src/config.rs b/actix-web/src/config.rs index 68bea34ca..11eaf8720 100644 --- a/actix-web/src/config.rs +++ b/actix-web/src/config.rs @@ -141,7 +141,7 @@ impl AppConfig { self.secure } - /// Returns the socket address of the local half of this TCP connection + /// Returns the socket address of the local half of this TCP connection. pub fn local_addr(&self) -> SocketAddr { self.addr } diff --git a/actix-web/src/guard/host.rs b/actix-web/src/guard/host.rs new file mode 100644 index 000000000..f05c81183 --- /dev/null +++ b/actix-web/src/guard/host.rs @@ -0,0 +1,209 @@ +use actix_http::{header, uri::Uri, RequestHead}; + +use super::{Guard, GuardContext}; + +/// Creates a guard that matches requests targetting a specific host. +/// +/// # Matching Host +/// This guard will: +/// - match against the `Host` header, if present; +/// - fall-back to matching against the request target's host, if present; +/// - return false if host cannot be determined; +/// +/// # Matching Scheme +/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using +/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent +/// the guard from matching successfully. +/// +/// # Examples +/// The `Host` guard can be used to set up a form of [virtual hosting] within a single app. +/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard +/// definitions they become safe to use in this way. Without these host guards, only routes under +/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` +/// and `localhost` as the `Host` guards. +/// ``` +/// use actix_web::{web, http::Method, guard, App, HttpResponse}; +/// +/// App::new() +/// .service( +/// web::scope("") +/// .guard(guard::Host("www.rust-lang.org")) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("marketing site") +/// })), +/// ) +/// .service( +/// web::scope("") +/// .guard(guard::Host("play.rust-lang.org")) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("playground frontend") +/// })), +/// ); +/// ``` +/// +/// The example below additionally guards on the host URI's scheme. This could allow routing to +/// different handlers for `http:` vs `https:` visitors; to redirect, for example. +/// ``` +/// use actix_web::{web, guard::Host, HttpResponse}; +/// +/// web::scope("/admin") +/// .guard(Host("admin.rust-lang.org").scheme("https")) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("admin connection is secure") +/// })); +/// ``` +/// +/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting +#[allow(non_snake_case)] +pub fn Host(host: impl AsRef) -> HostGuard { + HostGuard { + host: host.as_ref().to_string(), + scheme: None, + } +} + +fn get_host_uri(req: &RequestHead) -> Option { + req.headers + .get(header::HOST) + .and_then(|host_value| host_value.to_str().ok()) + .or_else(|| req.uri.host()) + .and_then(|host| host.parse().ok()) +} + +#[doc(hidden)] +pub struct HostGuard { + host: String, + scheme: Option, +} + +impl HostGuard { + /// Set request scheme to match + pub fn scheme>(mut self, scheme: H) -> HostGuard { + self.scheme = Some(scheme.as_ref().to_string()); + self + } +} + +impl Guard for HostGuard { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + // parse host URI from header or request target + let req_host_uri = match get_host_uri(ctx.head()) { + Some(uri) => uri, + + // no match if host cannot be determined + None => return false, + }; + + match req_host_uri.host() { + // fall through to scheme checks + Some(uri_host) if self.host == uri_host => {} + + // Either: + // - request's host does not match guard's host; + // - It was possible that the parsed URI from request target did not contain a host. + _ => return false, + } + + if let Some(ref scheme) = self.scheme { + if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { + return scheme == req_host_uri_scheme; + } + + // TODO: is this the correct behavior? + // falls through if scheme cannot be determined + } + + // all conditions passed + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::TestRequest; + + #[test] + fn host_from_header() { + let req = TestRequest::default() + .insert_header(( + header::HOST, + header::HeaderValue::from_static("www.rust-lang.org"), + )) + .to_srv_request(); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } + + #[test] + fn host_without_header() { + let req = TestRequest::default() + .uri("www.rust-lang.org") + .to_srv_request(); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } + + #[test] + fn host_scheme() { + let req = TestRequest::default() + .insert_header(( + header::HOST, + header::HeaderValue::from_static("https://www.rust-lang.org"), + )) + .to_srv_request(); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("http"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } +} diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index e086f8648..164032bdc 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -52,12 +52,15 @@ use std::{ rc::Rc, }; -use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead}; +use actix_http::{header, Extensions, Method as HttpMethod, RequestHead}; use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _}; mod acceptable; +mod host; + pub use self::acceptable::Acceptable; +pub use self::host::{Host, HostGuard}; /// Provides access to request parts that are useful during routing. #[derive(Debug)] @@ -371,124 +374,6 @@ impl Guard for HeaderGuard { } } -/// Creates a guard that matches requests targetting a specific host. -/// -/// # Matching Host -/// This guard will: -/// - match against the `Host` header, if present; -/// - fall-back to matching against the request target's host, if present; -/// - return false if host cannot be determined; -/// -/// # Matching Scheme -/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using -/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent -/// the guard from matching successfully. -/// -/// # Examples -/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards. -/// -/// The example below additionally guards on the host URI's scheme. This could allow routing to -/// different handlers for `http:` vs `https:` visitors; to redirect, for example. -/// ``` -/// use actix_web::{web, guard::Host, HttpResponse}; -/// -/// web::scope("/admin") -/// .guard(Host("admin.rust-lang.org").scheme("https")) -/// .default_service(web::to(|| async { -/// HttpResponse::Ok().body("admin connection is secure") -/// })); -/// ``` -/// -/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app. -/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard -/// definitions they become safe to use in this way. Without these host guards, only routes under -/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` -/// and `localhost` as the `Host` guards. -/// ``` -/// use actix_web::{web, http::Method, guard, App, HttpResponse}; -/// -/// App::new() -/// .service( -/// web::scope("") -/// .guard(guard::Host("www.rust-lang.org")) -/// .default_service(web::to(|| async { -/// HttpResponse::Ok().body("marketing site") -/// })), -/// ) -/// .service( -/// web::scope("") -/// .guard(guard::Host("play.rust-lang.org")) -/// .default_service(web::to(|| async { -/// HttpResponse::Ok().body("playground frontend") -/// })), -/// ); -/// ``` -/// -/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting -#[allow(non_snake_case)] -pub fn Host(host: impl AsRef) -> HostGuard { - HostGuard { - host: host.as_ref().to_string(), - scheme: None, - } -} - -fn get_host_uri(req: &RequestHead) -> Option { - req.headers - .get(header::HOST) - .and_then(|host_value| host_value.to_str().ok()) - .or_else(|| req.uri.host()) - .and_then(|host| host.parse().ok()) -} - -#[doc(hidden)] -pub struct HostGuard { - host: String, - scheme: Option, -} - -impl HostGuard { - /// Set request scheme to match - pub fn scheme>(mut self, scheme: H) -> HostGuard { - self.scheme = Some(scheme.as_ref().to_string()); - self - } -} - -impl Guard for HostGuard { - fn check(&self, ctx: &GuardContext<'_>) -> bool { - // parse host URI from header or request target - let req_host_uri = match get_host_uri(ctx.head()) { - Some(uri) => uri, - - // no match if host cannot be determined - None => return false, - }; - - match req_host_uri.host() { - // fall through to scheme checks - Some(uri_host) if self.host == uri_host => {} - - // Either: - // - request's host does not match guard's host; - // - It was possible that the parsed URI from request target did not contain a host. - _ => return false, - } - - if let Some(ref scheme) = self.scheme { - if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { - return scheme == req_host_uri_scheme; - } - - // TODO: is this the correct behavior? - // falls through if scheme cannot be determined - } - - // all conditions passed - true - } -} - #[cfg(test)] mod tests { use actix_http::{header, Method}; @@ -515,90 +400,6 @@ mod tests { assert!(!hdr.check(&req.guard_ctx())); } - #[test] - fn host_from_header() { - let req = TestRequest::default() - .insert_header(( - header::HOST, - header::HeaderValue::from_static("www.rust-lang.org"), - )) - .to_srv_request(); - - let host = Host("www.rust-lang.org"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org").scheme("https"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("crates.io"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("localhost"); - assert!(!host.check(&req.guard_ctx())); - } - - #[test] - fn host_without_header() { - let req = TestRequest::default() - .uri("www.rust-lang.org") - .to_srv_request(); - - let host = Host("www.rust-lang.org"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org").scheme("https"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("crates.io"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("localhost"); - assert!(!host.check(&req.guard_ctx())); - } - - #[test] - fn host_scheme() { - let req = TestRequest::default() - .insert_header(( - header::HOST, - header::HeaderValue::from_static("https://www.rust-lang.org"), - )) - .to_srv_request(); - - let host = Host("www.rust-lang.org").scheme("https"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org").scheme("http"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("crates.io").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("localhost"); - assert!(!host.check(&req.guard_ctx())); - } - #[test] fn method_guards() { let get_req = TestRequest::get().to_srv_request(); diff --git a/actix-web/src/info.rs b/actix-web/src/info.rs index 7c685406e..c5d9638f4 100644 --- a/actix-web/src/info.rs +++ b/actix-web/src/info.rs @@ -76,7 +76,6 @@ impl ConnectionInfo { for (name, val) in req .headers .get_all(&header::FORWARDED) - .into_iter() .filter_map(|hdr| hdr.to_str().ok()) // "for=1.2.3.4, for=5.6.7.8; scheme=https" .flat_map(|val| val.split(';')) diff --git a/actix-web/src/middleware/authors-guide.md b/actix-web/src/middleware/authors-guide.md index a8d1edea4..64bad15c2 100644 --- a/actix-web/src/middleware/authors-guide.md +++ b/actix-web/src/middleware/authors-guide.md @@ -13,4 +13,5 @@ ## When To (Not) Use Middleware ## Author's References + - `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428 diff --git a/actix-web/src/middleware/err_handlers.rs b/actix-web/src/middleware/err_handlers.rs index 4ddbc6318..00fb5cf59 100644 --- a/actix-web/src/middleware/err_handlers.rs +++ b/actix-web/src/middleware/err_handlers.rs @@ -50,6 +50,8 @@ type DefaultHandler = Option>>; /// will pass by unchanged by this middleware. /// /// # Examples +/// ## Handler Response +/// Header /// ``` /// use actix_web::http::{header, StatusCode}; /// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; @@ -67,6 +69,28 @@ type DefaultHandler = Option>>; /// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header)) /// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` +/// +/// Body Content +/// ``` +/// use actix_web::http::{header, StatusCode}; +/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; +/// use actix_web::{dev, web, App, HttpResponse, Result}; +/// fn add_error_body(res: dev::ServiceResponse) -> Result> { +/// // Get the error message and status code +/// let error_message = "An error occurred"; +/// // Destructures ServiceResponse into request and response components +/// let (req, res) = res.into_parts(); +/// // Create a new response with the modified body +/// let res = res.set_body(error_message).map_into_boxed_body(); +/// // Create a new ServiceResponse with the modified response +/// let res = dev::ServiceResponse::new(req, res).map_into_right_body(); +/// Ok(ErrorHandlerResponse::Response(res)) +///} +/// +/// let app = App::new() +/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_body)) +/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); +/// ``` /// ## Registering default handler /// ``` /// # use actix_web::http::{header, StatusCode}; diff --git a/actix-web/src/request.rs b/actix-web/src/request.rs index 6a32bf838..a99dcaa3e 100644 --- a/actix-web/src/request.rs +++ b/actix-web/src/request.rs @@ -260,7 +260,7 @@ impl HttpRequest { Ref::map(self.extensions(), |data| data.get().unwrap()) } - /// App config + /// Returns a reference to the application's connection configuration. #[inline] pub fn app_config(&self) -> &AppConfig { self.app_state().config() diff --git a/actix-web/src/response/responder.rs b/actix-web/src/response/responder.rs index da8091981..965163a1f 100644 --- a/actix-web/src/response/responder.rs +++ b/actix-web/src/response/responder.rs @@ -21,7 +21,7 @@ use crate::{Error, HttpRequest, HttpResponse}; /// - `HttpResponse` and `HttpResponseBuilder` /// - `Option` where `R: Responder` /// - `Result` where `R: Responder` and [`E: ResponseError`](crate::ResponseError) -/// - `(R, StatusCode) where `R: Responder` +/// - `(R, StatusCode)` where `R: Responder` /// - `&'static str`, `String`, `&'_ String`, `Cow<'_, str>`, [`ByteString`](bytestring::ByteString) /// - `&'static [u8]`, `Vec`, `Bytes`, `BytesMut` /// - [`Json`](crate::web::Json) and [`Form`](crate::web::Form) where `T: Serialize` diff --git a/actix-web/src/service.rs b/actix-web/src/service.rs index ea23f09f5..f7692ce16 100644 --- a/actix-web/src/service.rs +++ b/actix-web/src/service.rs @@ -238,11 +238,7 @@ impl ServiceRequest { self.req.connection_info() } - /// Returns reference to the Path parameters. - /// - /// Params is a container for URL parameters. A variable segment is specified in the form - /// `{identifier}`, where the identifier can be used later in a request handler to access the - /// matched value for that segment. + /// Counterpart to [`HttpRequest::match_info`]. #[inline] pub fn match_info(&self) -> &Path { self.req.match_info() @@ -267,12 +263,13 @@ impl ServiceRequest { } /// Returns a reference to the application's resource map. + /// Counterpart to [`HttpRequest::resource_map`]. #[inline] pub fn resource_map(&self) -> &ResourceMap { self.req.resource_map() } - /// Returns a reference to the application's configuration. + /// Counterpart to [`HttpRequest::app_config`]. #[inline] pub fn app_config(&self) -> &AppConfig { self.req.app_config() diff --git a/actix-web/src/test/mod.rs b/actix-web/src/test/mod.rs index 9c6121151..5d9367b82 100644 --- a/actix-web/src/test/mod.rs +++ b/actix-web/src/test/mod.rs @@ -10,12 +10,16 @@ //! # Calling Test Service //! - [`TestRequest`] //! - [`call_service`] +//! - [`try_call_service`] //! - [`call_and_read_body`] //! - [`call_and_read_body_json`] +//! - [`try_call_and_read_body_json`] //! //! # Reading Response Payloads //! - [`read_body`] +//! - [`try_read_body`] //! - [`read_body_json`] +//! - [`try_read_body_json`] // TODO: more docs on generally how testing works with these parts @@ -31,7 +35,8 @@ pub use self::test_services::{default_service, ok_service, simple_service, statu #[allow(deprecated)] pub use self::test_utils::{ call_and_read_body, call_and_read_body_json, call_service, init_service, read_body, - read_body_json, read_response, read_response_json, + read_body_json, read_response, read_response_json, try_call_and_read_body_json, + try_call_service, try_read_body, try_read_body_json, }; #[cfg(test)] diff --git a/actix-web/src/test/test_utils.rs b/actix-web/src/test/test_utils.rs index 6f0926f35..b985c3b36 100644 --- a/actix-web/src/test/test_utils.rs +++ b/actix-web/src/test/test_utils.rs @@ -100,6 +100,15 @@ where .expect("test service call returned error") } +/// Fallible version of [`call_service`] that allows testing response completion errors. +pub async fn try_call_service(app: &S, req: R) -> Result +where + S: Service, Error = E>, + E: std::fmt::Debug, +{ + app.call(req).await +} + /// Helper function that returns a response body of a TestRequest /// /// # Examples @@ -185,13 +194,23 @@ pub async fn read_body(res: ServiceResponse) -> Bytes where B: MessageBody, { - let body = res.into_body(); - body::to_bytes(body) + try_read_body(res) .await .map_err(Into::>::into) .expect("error reading test response body") } +/// Fallible version of [`read_body`] that allows testing MessageBody reading errors. +pub async fn try_read_body( + res: ServiceResponse, +) -> Result::Error> +where + B: MessageBody, +{ + let body = res.into_body(); + body::to_bytes(body).await +} + /// Helper function that returns a deserialized response body of a ServiceResponse. /// /// # Examples @@ -240,18 +259,27 @@ where B: MessageBody, T: DeserializeOwned, { - let body = read_body(res).await; - - serde_json::from_slice(&body).unwrap_or_else(|err| { + try_read_body_json(res).await.unwrap_or_else(|err| { panic!( - "could not deserialize body into a {}\nerr: {}\nbody: {:?}", + "could not deserialize body into a {}\nerr: {}", std::any::type_name::(), err, - body, ) }) } +/// Fallible version of [`read_body_json`] that allows testing response deserialization errors. +pub async fn try_read_body_json(res: ServiceResponse) -> Result> +where + B: MessageBody, + T: DeserializeOwned, +{ + let body = try_read_body(res) + .await + .map_err(Into::>::into)?; + serde_json::from_slice(&body).map_err(Into::>::into) +} + /// Helper function that returns a deserialized response body of a TestRequest /// /// # Examples @@ -299,8 +327,23 @@ where B: MessageBody, T: DeserializeOwned, { - let res = call_service(app, req).await; - read_body_json(res).await + try_call_and_read_body_json(app, req).await.unwrap() +} + +/// Fallible version of [`call_and_read_body_json`] that allows testing service call errors. +pub async fn try_call_and_read_body_json( + app: &S, + req: Request, +) -> Result> +where + S: Service, Error = Error>, + B: MessageBody, + T: DeserializeOwned, +{ + let res = try_call_service(app, req) + .await + .map_err(Into::>::into)?; + try_read_body_json(res).await } #[doc(hidden)] @@ -358,7 +401,7 @@ mod tests { assert_eq!(result, Bytes::from_static(b"delete!")); } - #[derive(Serialize, Deserialize)] + #[derive(Serialize, Deserialize, Debug)] pub struct Person { id: String, name: String, @@ -383,6 +426,26 @@ mod tests { assert_eq!(&result.id, "12345"); } + #[actix_rt::test] + async fn test_try_response_json_error() { + let app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; + + let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); + + let req = TestRequest::post() + .uri("/animals") // Not registered to ensure an error occurs. + .insert_header((header::CONTENT_TYPE, "application/json")) + .set_payload(payload) + .to_request(); + + let result: Result> = + try_call_and_read_body_json(&app, req).await; + assert!(result.is_err()); + } + #[actix_rt::test] async fn test_body_json() { let app = init_service(App::new().service(web::resource("/people").route( @@ -403,6 +466,27 @@ mod tests { assert_eq!(&result.name, "User name"); } + #[actix_rt::test] + async fn test_try_body_json_error() { + let app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; + + // Use a number for id to cause a deserialization error. + let payload = r#"{"id":12345,"name":"User name"}"#.as_bytes(); + + let res = TestRequest::post() + .uri("/people") + .insert_header((header::CONTENT_TYPE, "application/json")) + .set_payload(payload) + .send_request(&app) + .await; + + let result: Result> = try_read_body_json(res).await; + assert!(result.is_err()); + } + #[actix_rt::test] async fn test_request_response_form() { let app = init_service(App::new().service(web::resource("/people").route( diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 7892d9339..03cbf61d4 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,22 +1,35 @@ # Changes -## Unreleased - 2022-xx-xx +## Unreleased - 2023-xx-xx + +## 3.1.1 - 2023-02-26 + ### Changed + +- `client::Connect` is now public to allow tunneling connection with `client::Connector`. + +## 3.1.0 - 2023-01-21 + +### Changed + - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. - ## 3.0.1 - 2022-08-25 + ### Changed + - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. ### Fixed + - Fixed handling of redirection requests that begin with `//`. [#2840] [#2840]: https://github.com/actix/actix-web/pull/2840 - ## 3.0.0 - 2022-03-07 + ### Dependencies + - Updated `actix-*` to Tokio v1-based versions. [#1813] - Updated `bytes` to `1.0`. [#1813] - Updated `cookie` to `0.16`. [#2555] @@ -25,6 +38,7 @@ - Updated `tokio` to `1`. ### Added + - `trust-dns` crate feature to enable `trust-dns-resolver` as client DNS resolver; disabled by default. [#1969] - `cookies` crate feature; enabled by default. [#2619] - `compress-brotli` crate feature; enabled by default. [#2250] @@ -41,6 +55,7 @@ - `ClientBuilder::add_default_header()` (and deprecate `ClientBuilder::header()`). [#2510] ### Changed + - `client::Connector` type now only has one generic type for `actix_service::Service`. [#2063] - `client::error::ConnectError` Resolver variant contains `Box` type. [#1905] - `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] @@ -58,6 +73,7 @@ - Minimum supported Rust version (MSRV) is now 1.54. ### Fixed + - Send headers along with redirected requests. [#2310] - Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] - Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] @@ -66,6 +82,7 @@ - `impl Stream` for `ClientResponse` no longer requires the body type be `Unpin`. [#2546] ### Removed + - `compress` crate feature. [#2250] - `ClientRequest::set`; use `ClientRequest::insert_header`. [#1869] - `ClientRequest::set_header`; use `ClientRequest::insert_header`. [#1869] @@ -75,10 +92,10 @@ - `ClientBuilder::default` function [#2008] ### Security + - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. -[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html - +[`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html [#1813]: https://github.com/actix/actix-web/pull/1813 [#1869]: https://github.com/actix/actix-web/pull/1869 [#1905]: https://github.com/actix/actix-web/pull/1905 @@ -108,46 +125,48 @@ [#2553]: https://github.com/actix/actix-web/pull/2553 [#2555]: https://github.com/actix/actix-web/pull/2555 -
3.0.0 Pre-Releases ## 3.0.0-beta.21 - 2022-02-16 + - No significant changes since `3.0.0-beta.20`. - ## 3.0.0-beta.20 - 2022-01-31 + - No significant changes since `3.0.0-beta.19`. - ## 3.0.0-beta.19 - 2022-01-21 + - No significant changes since `3.0.0-beta.18`. - ## 3.0.0-beta.18 - 2022-01-04 + - Minimum supported Rust version (MSRV) is now 1.54. - ## 3.0.0-beta.17 - 2021-12-29 + ### Changed + - Update `cookie` dependency (re-exported) to `0.16`. [#2555] ### Security + - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. [#2555]: https://github.com/actix/actix-web/pull/2555 -[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html - +[`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html ## 3.0.0-beta.16 - 2021-12-29 + - `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553] - `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553] - Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] [#2553]: https://github.com/actix/actix-web/pull/2553 - ## 3.0.0-beta.15 - 2021-12-27 + - Rename `Connector::{ssl => openssl}`. [#2503] - Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] - `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546] @@ -159,89 +178,96 @@ [#2503]: https://github.com/actix/actix-web/pull/2503 [#2546]: https://github.com/actix/actix-web/pull/2546 - ## 3.0.0-beta.14 - 2021-12-17 + - Add `ClientBuilder::add_default_header` and deprecate `ClientBuilder::header`. [#2510] [#2510]: https://github.com/actix/actix-web/pull/2510 - ## 3.0.0-beta.13 - 2021-12-11 + - No significant changes since `3.0.0-beta.12`. - ## 3.0.0-beta.12 - 2021-11-30 + - Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2474]: https://github.com/actix/actix-web/pull/2474 - ## 3.0.0-beta.11 - 2021-11-22 + - No significant changes from `3.0.0-beta.10`. - ## 3.0.0-beta.10 - 2021-11-15 + - No significant changes from `3.0.0-beta.9`. - ## 3.0.0-beta.9 - 2021-10-20 + - Updated rustls to v0.20. [#2414] [#2414]: https://github.com/actix/actix-web/pull/2414 - ## 3.0.0-beta.8 - 2021-09-09 + ### Changed + - Send headers within the redirect requests. [#2310] [#2310]: https://github.com/actix/actix-web/pull/2310 - ## 3.0.0-beta.7 - 2021-06-26 + ### Changed + - Change compression algorithm features flags. [#2250] [#2250]: https://github.com/actix/actix-web/pull/2250 - ## 3.0.0-beta.6 - 2021-06-17 + - No significant changes since 3.0.0-beta.5. - ## 3.0.0-beta.5 - 2021-04-17 + ### Removed + - Deprecated methods on `ClientRequest`: `if_true`, `if_some`. [#2148] [#2148]: https://github.com/actix/actix-web/pull/2148 - ## 3.0.0-beta.4 - 2021-04-02 + ### Added + - Add `Client::headers` to get default mut reference of `HeaderMap` of client object. [#2114] ### Changed + - `ConnectorService` type is renamed to `BoxConnectorService`. [#2081] - Fix http/https encoding when enabling `compress` feature. [#2116] -- Rename `TestResponse::header` to `append_header`, `set` to `insert_header`. `TestResponse` header - methods now take `TryIntoHeaderPair` tuples. [#2094] +- Rename `TestResponse::header` to `append_header`, `set` to `insert_header`. `TestResponse` header methods now take `TryIntoHeaderPair` tuples. [#2094] [#2081]: https://github.com/actix/actix-web/pull/2081 [#2094]: https://github.com/actix/actix-web/pull/2094 [#2114]: https://github.com/actix/actix-web/pull/2114 [#2116]: https://github.com/actix/actix-web/pull/2116 - ## 3.0.0-beta.3 - 2021-03-08 + ### Added + - `ClientResponse::timeout` for set the timeout of collecting response body. [#1931] - `ClientBuilder::local_address` for bind to a local ip address for this client. [#2024] ### Changed + - Feature `cookies` is now optional and enabled by default. [#1981] - `ClientBuilder::connector` method would take `actix_http::client::Connector` type. [#2008] - Basic auth password now takes blank passwords as an empty string instead of Option. [#2050] ### Removed + - `ClientBuilder::default` function [#2008] [#1931]: https://github.com/actix/actix-web/pull/1931 @@ -250,17 +276,20 @@ [#2024]: https://github.com/actix/actix-web/pull/2024 [#2050]: https://github.com/actix/actix-web/pull/2050 - ## 3.0.0-beta.2 - 2021-02-10 + ### Added + - `ClientRequest::insert_header` method which allows using typed headers. [#1869] - `ClientRequest::append_header` method which allows using typed headers. [#1869] - `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] ### Changed + - Relax default timeout for `Connector` to 5 seconds(original 1 second). [#1905] ### Removed + - `ClientRequest::set`; use `ClientRequest::insert_header`. [#1869] - `ClientRequest::set_header`; use `ClientRequest::insert_header`. [#1869] - `ClientRequest::set_header_if_none`; use `ClientRequest::insert_header_if_none`. [#1869] @@ -270,9 +299,10 @@ [#1905]: https://github.com/actix/actix-web/pull/1905 [#1969]: https://github.com/actix/actix-web/pull/1969 - ## 3.0.0-beta.1 - 2021-01-07 + ### Changed + - Update `rand` to `0.8` - Update `bytes` to `1.0`. [#1813] - Update `rust-tls` to `0.19`. [#1813] @@ -282,53 +312,62 @@
## 2.0.3 - 2020-11-29 + ### Fixed + - Ensure `actix-http` dependency uses same `serde_urlencoded`. - ## 2.0.2 - 2020-11-25 + ### Changed + - Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 - ## 2.0.1 - 2020-10-30 + ### Changed + - Upgrade `base64` to `0.13`. [#1744] - Deprecate `ClientRequest::{if_some, if_true}`. [#1760] ### Fixed -- Use `Accept-Encoding: identity` instead of `Accept-Encoding: br` when no compression feature - is enabled [#1737] + +- Use `Accept-Encoding: identity` instead of `Accept-Encoding: br` when no compression feature is enabled [#1737] [#1737]: https://github.com/actix/actix-web/pull/1737 [#1760]: https://github.com/actix/actix-web/pull/1760 [#1744]: https://github.com/actix/actix-web/pull/1744 - ## 2.0.0 - 2020-09-11 + ### Changed + - `Client::build` was renamed to `Client::builder`. - ## 2.0.0-beta.4 - 2020-09-09 + ### Changed + - Update actix-codec & actix-tls dependencies. - ## 2.0.0-beta.3 - 2020-08-17 + ### Changed + - Update `rustls` to 0.18 - ## 2.0.0-beta.2 - 2020-07-21 + ### Changed + - Update `actix-http` dependency to 2.0.0-beta.2 - ## [2.0.0-beta.1] - 2020-07-14 + ### Changed + - Update `actix-http` dependency to 2.0.0-beta.1 ## [2.0.0-alpha.2] - 2020-05-21 @@ -360,26 +399,22 @@ - Migrate to `std::future` - ## [0.2.8] - 2019-11-06 - Add support for setting query from Serialize type for client request. - ## [0.2.7] - 2019-09-25 ### Added - Remaining getter methods for `ClientRequest`'s private `head` field #1101 - ## [0.2.6] - 2019-09-12 ### Added - Export frozen request related types. - ## [0.2.5] - 2019-09-11 ### Added @@ -390,7 +425,6 @@ - Ensure that the `Host` header is set when initiating a WebSocket client connection. - ## [0.2.4] - 2019-08-13 ### Changed @@ -399,14 +433,12 @@ - Update serde_urlencoded to "0.6.1" - ## [0.2.3] - 2019-08-01 ### Added - Add `rustls` support - ## [0.2.2] - 2019-07-01 ### Changed @@ -415,7 +447,6 @@ - Upgrade `rand` dependency version to 0.7 - ## [0.2.1] - 2019-06-05 ### Added @@ -432,7 +463,6 @@ - Upgrade actix-http dependency. - ## [0.1.1] - 2019-04-19 ### Added @@ -443,19 +473,16 @@ - `ClientRequest::if_true()` and `ClientRequest::if_some()` use instance instead of ref - ## [0.1.0] - 2019-04-16 - No changes - ## [0.1.0-alpha.6] - 2019-04-14 ### Changed - Do not set default headers for websocket request - ## [0.1.0-alpha.5] - 2019-04-12 ### Changed @@ -466,14 +493,12 @@ - Add Debug impl for BoxedSocket - ## [0.1.0-alpha.4] - 2019-04-08 ### Changed - Update actix-http dependency - ## [0.1.0-alpha.3] - 2019-04-02 ### Added @@ -482,7 +507,6 @@ - `ClientResponse::json()` - Loads and parse `application/json` encoded body - ### Changed - `ClientRequest::json()` accepts reference instead of object. @@ -491,7 +515,6 @@ - Renamed `ClientRequest::close_connection()` to `ClientRequest::force_close()` - ## [0.1.0-alpha.2] - 2019-03-29 ### Added @@ -504,14 +527,12 @@ - Re-export `actix_http::client::Connector`. - ### Changed - Allow to override request's uri - Export `ws` sub-module with websockets related types - ## [0.1.0-alpha.1] - 2019-03-28 - Initial impl diff --git a/awc/Cargo.toml b/awc/Cargo.toml index cf64eed49..8a75e28f7 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.1" +version = "3.1.1" authors = ["Nikolay Kim "] description = "Async HTTP and WebSocket client library" keywords = ["actix", "http", "framework", "async", "web"] @@ -57,13 +57,12 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.5" actix-service = "2" -actix-http = { version = "3", features = ["http2", "ws"] } +actix-http = { version = "3.3", features = ["http2", "ws"] } actix-rt = { version = "2.1", default-features = false } actix-tls = { version = "3", features = ["connect", "uri"] } actix-utils = "3" -ahash = "0.7" -base64 = "0.13" +base64 = "0.21" bytes = "1" cfg-if = "1" derive_more = "0.99.5" @@ -80,7 +79,7 @@ rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" -tokio = { version = "1.8.4", features = ["sync"] } +tokio = { version = "1.24.2", features = ["sync"] } cookie = { version = "0.16", features = ["percent-encode"], optional = true } @@ -99,14 +98,14 @@ actix-utils = "3" actix-web = { version = "4", features = ["openssl"] } brotli = "3.3.3" -const-str = "0.4" +const-str = "0.3" env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false } static_assertions = "1.1" rcgen = "0.9" rustls-pemfile = "1" -tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } zstd = "0.12" [[example]] diff --git a/awc/README.md b/awc/README.md index 9f47e663b..a9d411067 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,16 +3,16 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.1)](https://docs.rs/awc/3.0.1) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.1.1)](https://docs.rs/awc/3.1.1) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.1/status.svg)](https://deps.rs/crate/awc/3.0.1) +[![Dependency Status](https://deps.rs/crate/awc/3.1.1/status.svg)](https://deps.rs/crate/awc/3.1.1) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/awc) - [Example Project](https://github.com/actix/examples/tree/master/https-tls/awc-https) -- Minimum Supported Rust Version (MSRV): 1.54 +- Minimum Supported Rust Version (MSRV): 1.59 ## Example diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 34a5f8505..79838a3f6 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -1,5 +1,7 @@ use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration}; +use base64::prelude::*; + use actix_http::{ error::HttpError, header::{self, HeaderMap, HeaderName, TryIntoHeaderPair}, @@ -210,7 +212,7 @@ where }; self.add_default_header(( header::AUTHORIZATION, - format!("Basic {}", base64::encode(auth)), + format!("Basic {}", BASE64_STANDARD.encode(auth)), )) } diff --git a/awc/src/client/pool.rs b/awc/src/client/pool.rs index 47c1fdd67..632608c45 100644 --- a/awc/src/client/pool.rs +++ b/awc/src/client/pool.rs @@ -2,7 +2,7 @@ use std::{ cell::RefCell, - collections::VecDeque, + collections::{HashMap, VecDeque}, future::Future, io, ops::Deref, @@ -17,7 +17,6 @@ use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; use actix_http::Protocol; use actix_rt::time::{sleep, Sleep}; use actix_service::Service; -use ahash::AHashMap; use futures_core::future::LocalBoxFuture; use futures_util::FutureExt as _; use http::uri::Authority; @@ -62,7 +61,7 @@ where { fn new(config: ConnectorConfig) -> Self { let permits = Arc::new(Semaphore::new(config.limit)); - let available = RefCell::new(AHashMap::default()); + let available = RefCell::new(HashMap::default()); Self(Rc::new(ConnectionPoolInnerPriv { config, @@ -124,7 +123,7 @@ where Io: AsyncWrite + Unpin + 'static, { config: ConnectorConfig, - available: RefCell>>>, + available: RefCell>>>, permits: Arc, } diff --git a/awc/src/lib.rs b/awc/src/lib.rs index bb7f06c93..42f029669 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -139,7 +139,7 @@ pub mod http { } pub use self::builder::ClientBuilder; -pub use self::client::{Client, Connector}; +pub use self::client::{Client, Connect, Connector}; pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::request::ClientRequest; diff --git a/awc/src/request.rs b/awc/src/request.rs index 331c80af7..d3a4eda8c 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,5 +1,6 @@ use std::{convert::TryFrom, fmt, net, rc::Rc, time::Duration}; +use base64::prelude::*; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; @@ -238,7 +239,7 @@ impl ClientRequest { self.insert_header(( header::AUTHORIZATION, - format!("Basic {}", base64::encode(auth)), + format!("Basic {}", BASE64_STANDARD.encode(auth)), )) } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index f905b8ef2..406368e62 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -28,6 +28,8 @@ use std::{convert::TryFrom, fmt, net::SocketAddr, str}; +use base64::prelude::*; + use actix_codec::Framed; use actix_http::{ws, Payload, RequestHead}; use actix_rt::time::timeout; @@ -236,7 +238,10 @@ impl WebsocketsRequest { Some(password) => format!("{}:{}", username, password), None => format!("{}:", username), }; - self.header(AUTHORIZATION, format!("Basic {}", base64::encode(auth))) + self.header( + AUTHORIZATION, + format!("Basic {}", BASE64_STANDARD.encode(auth)), + ) } /// Set HTTP bearer authentication header @@ -321,7 +326,7 @@ impl WebsocketsRequest { // Generate a random key for the `Sec-WebSocket-Key` header which is a base64-encoded // (see RFC 4648 §4) value that, when decoded, is 16 bytes in length (RFC 6455 §1.3). let sec_key: [u8; 16] = rand::random(); - let key = base64::encode(sec_key); + let key = BASE64_STANDARD.encode(sec_key); self.head.headers.insert( header::SEC_WEBSOCKET_KEY, diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 0949595cb..9c3543ff0 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -13,6 +13,7 @@ use std::{ }; use actix_utils::future::ok; +use base64::prelude::*; use bytes::Bytes; use cookie::Cookie; use futures_util::stream; @@ -783,7 +784,7 @@ async fn client_basic_auth() { .unwrap() .to_str() .unwrap() - == format!("Basic {}", base64::encode("username:password")) + == format!("Basic {}", BASE64_STANDARD.encode("username:password")) { HttpResponse::Ok() } else { diff --git a/scripts/bump b/scripts/bump index 33ea52010..40d43d429 100755 --- a/scripts/bump +++ b/scripts/bump @@ -51,7 +51,6 @@ cat "$CHANGELOG_FILE" | if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then echo "- No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE" echo >>"$CHANGE_CHUNK_FILE" - echo >>"$CHANGE_CHUNK_FILE" fi if [ -n "${2-}" ]; then @@ -82,7 +81,6 @@ sed -i.bak -E "s/^version ?= ?\"[^\"]+\"$/version = \"$NEW_VERSION\"/" "$CARGO_M ( sed '/Unreleased/ q' "$CHANGELOG_FILE" # up to unreleased heading echo # blank line - echo # blank line echo "## $NEW_VERSION - $DATE" # new version heading cat "$CHANGE_CHUNK_FILE" # previously unreleased changes sed "/$CURRENT_VERSION/ q" "$CHANGELOG_FILE" | tail -n 1 # the previous version heading @@ -90,6 +88,9 @@ sed -i.bak -E "s/^version ?= ?\"[^\"]+\"$/version = \"$NEW_VERSION\"/" "$CARGO_M ) >"$CHANGELOG_FILE.bak" mv "$CHANGELOG_FILE.bak" "$CHANGELOG_FILE" +# format CHANGELOG file according to prettier +npx -y prettier --write "$CHANGELOG_FILE" || true + # done; remove backup files rm -f $CARGO_MANIFEST.bak rm -f $CHANGELOG_FILE.bak @@ -139,12 +140,14 @@ GIT_TAG="$(echo $SHORT_PACKAGE_NAME-v$NEW_VERSION)" RELEASE_TITLE="$(echo $PACKAGE_NAME: v$NEW_VERSION)" if [ "$(echo $NEW_VERSION | grep beta)" ] || [ "$(echo $NEW_VERSION | grep rc)" ] || [ "$(echo $NEW_VERSION | grep alpha)" ]; then - PRERELEASE="--prerelease" + FLAGS="--prerelease" +else + FLAGS="--latest" fi echo echo "GitHub release command:" -GH_CMD="gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" ${PRERELEASE:-}" +GH_CMD="gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" ${FLAGS:-}" echo "$GH_CMD" read -p "Submit draft GH release: (y/N) " GH_RELEASE