├── .envrc ├── README.md ├── russe ├── LICENSE-MIT ├── src │ ├── encoder.rs │ ├── event.rs │ ├── error.rs │ ├── lib.rs │ ├── message.rs │ ├── unix_lines.rs │ └── reqwest_0_12.rs ├── LICENSE-APACHE ├── CHANGELOG.md ├── README.md ├── examples │ ├── manager.rs │ └── decoder.rs ├── Cargo.toml └── benches │ ├── results.txt │ └── decoder.rs ├── actix-hash ├── LICENSE-MIT ├── LICENSE-APACHE ├── CHANGELOG.md ├── README.md ├── examples │ └── body_sha2.rs ├── Cargo.toml └── src │ └── lib.rs ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── coverage.yml │ ├── lint.yml │ └── ci.yml ├── actix-web-lab ├── LICENSE-MIT ├── LICENSE-APACHE ├── src │ ├── guard.rs │ ├── respond.rs │ ├── body.rs │ ├── test.rs │ ├── middleware.rs │ ├── test_services.rs │ ├── web.rs │ ├── extract.rs │ ├── cbor.rs │ ├── infallible_body_stream.rs │ ├── msgpack.rs │ ├── host.rs │ ├── body_async_write.rs │ ├── test_request_macros.rs │ ├── lib.rs │ ├── test_header_macros.rs │ ├── body_channel.rs │ ├── redirect_to_www.rs │ ├── redirect_to_non_www.rs │ ├── header.rs │ ├── display_stream.rs │ ├── csv.rs │ ├── condition_option.rs │ ├── swap_data.rs │ ├── ndjson.rs │ ├── util.rs │ ├── spa.rs │ ├── test_response_macros.rs │ ├── panic_reporter.rs │ └── catch_panic.rs ├── examples │ ├── assets │ │ ├── actix.png │ │ ├── spa.html │ │ └── sse.html │ ├── cbor.rs │ ├── msgpack.rs │ ├── spa.rs │ ├── body_channel.rs │ ├── map_response.rs │ ├── fork_request_payload.rs │ ├── body_hmac.rs │ ├── query.rs │ ├── sse.rs │ ├── ndjson.rs │ ├── from_fn.rs │ ├── body_async_write.rs │ ├── json.rs │ └── req_sig.rs └── Cargo.toml ├── actix-proxy-protocol ├── LICENSE-MIT ├── LICENSE-APACHE ├── CHANGELOG.md ├── README.md ├── Cargo.toml └── src │ └── v1 │ └── mod.rs ├── actix-client-ip-cloudflare ├── LICENSE-MIT ├── LICENSE-APACHE ├── CHANGELOG.md ├── examples │ ├── fetch-ips.rs │ └── extract-header.rs ├── README.md ├── Cargo.toml └── src │ ├── header_v4.rs │ ├── header_v6.rs │ ├── lib.rs │ └── extract.rs ├── .cargo └── config.toml ├── .clippy.toml ├── actix-web-lab-derive ├── README.md ├── tests │ ├── trybuild │ │ ├── err-invalid-structures.rs │ │ ├── ok-no-body-type.rs │ │ ├── ok-with-body-type.rs │ │ └── err-invalid-structures.stderr │ ├── trybuild.rs │ └── tdd.rs └── Cargo.toml ├── .prettierrc.yml ├── .rustfmt.toml ├── collectools ├── CHANGELOG.md ├── Cargo.toml └── src │ ├── lib.rs │ ├── vec.rs │ ├── arrayvec.rs │ ├── tinyvec.rs │ └── smallvec.rs ├── poll-add.sh ├── .gitignore ├── err-report ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── tests │ └── tests.rs ├── .taplo.toml ├── .cspell.yml ├── flake.nix ├── Cargo.toml ├── poll-results.sh ├── LICENSE-MIT ├── flake.lock └── justfile /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | actix-web-lab/README.md -------------------------------------------------------------------------------- /russe/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /russe/src/encoder.rs: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /actix-hash/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /russe/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [robjtede] 2 | -------------------------------------------------------------------------------- /actix-hash/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-web-lab/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-proxy-protocol/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-web-lab/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-proxy-protocol/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [net] 2 | git-fetch-with-cli = true 3 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /.clippy.toml: -------------------------------------------------------------------------------- 1 | disallowed-names = [ 2 | "e", # no single letter error bindings 3 | ] 4 | -------------------------------------------------------------------------------- /actix-web-lab-derive/README.md: -------------------------------------------------------------------------------- 1 | # actix-web-lab-derive 2 | 3 | > Experimental macros for Actix Web. 4 | -------------------------------------------------------------------------------- /actix-proxy-protocol/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## Unreleased 4 | 5 | ## 0.1.0 6 | 7 | - Initial release. 8 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | overrides: 2 | - files: "*.md" 3 | options: 4 | printWidth: 9999 5 | proseWrap: never 6 | -------------------------------------------------------------------------------- /actix-web-lab/src/guard.rs: -------------------------------------------------------------------------------- 1 | //! Experimental route guards. 2 | //! 3 | //! Analogous to the `guard` module in Actix Web. 4 | -------------------------------------------------------------------------------- /actix-web-lab/examples/assets/actix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robjtede/actix-web-lab/HEAD/actix-web-lab/examples/assets/actix.png -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Crate" 3 | use_field_init_shorthand = true 4 | format_code_in_doc_comments = true 5 | -------------------------------------------------------------------------------- /collectools/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | - Add `smallvec` crate support. 6 | 7 | ## 0.1.0 8 | 9 | - Initial release. 10 | -------------------------------------------------------------------------------- /actix-web-lab-derive/tests/trybuild/err-invalid-structures.rs: -------------------------------------------------------------------------------- 1 | use actix_web_lab::FromRequest; 2 | 3 | #[derive(FromRequest)] 4 | enum Foo { 5 | Data(()), 6 | } 7 | 8 | #[derive(FromRequest)] 9 | struct Bar(()); 10 | 11 | fn main() {} 12 | -------------------------------------------------------------------------------- /poll-add.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -euxo pipefail 4 | 5 | gh issue create \ 6 | --title="[poll] $1" \ 7 | --body="React to this issue with a \":+1:\" to vote for this feature. Highest voted features will graduate to Actix Web sooner." \ 8 | --label="poll" 9 | -------------------------------------------------------------------------------- /actix-web-lab-derive/tests/trybuild/ok-no-body-type.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{http, web}; 2 | use actix_web_lab_derive::FromRequest; 3 | 4 | #[derive(Debug, FromRequest)] 5 | struct RequestParts { 6 | method: http::Method, 7 | pool: web::Data, 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | - package-ecosystem: cargo 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | versioning-strategy: lockfile-only 12 | -------------------------------------------------------------------------------- /actix-web-lab/src/respond.rs: -------------------------------------------------------------------------------- 1 | //! Experimental responders and response helpers. 2 | 3 | #[cfg(feature = "cbor")] 4 | pub use crate::cbor::Cbor; 5 | #[cfg(feature = "msgpack")] 6 | pub use crate::msgpack::{MessagePack, MessagePackNamed}; 7 | pub use crate::{csv::Csv, display_stream::DisplayStream, ndjson::NdJson}; 8 | -------------------------------------------------------------------------------- /actix-web-lab/src/body.rs: -------------------------------------------------------------------------------- 1 | //! Experimental body types. 2 | //! 3 | //! Analogous to the `body` module in Actix Web. 4 | 5 | pub use crate::{ 6 | body_async_write::{Writer, writer}, 7 | body_channel::{Sender, channel}, 8 | infallible_body_stream::{new_infallible_body_stream, new_infallible_sized_stream}, 9 | }; 10 | -------------------------------------------------------------------------------- /actix-web-lab-derive/tests/trybuild/ok-with-body-type.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use actix_web::web; 4 | use actix_web_lab_derive::FromRequest; 5 | 6 | #[derive(Debug, FromRequest)] 7 | struct RequestParts { 8 | pool: web::Data, 9 | form: web::Json>, 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # direnv 13 | /.direnv 14 | 15 | # code coverage 16 | /codecov.json 17 | /lcov.info 18 | -------------------------------------------------------------------------------- /err-report/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## Unreleased 4 | 5 | ## 0.1.2 6 | 7 | - No significant changes since `0.1.1`. 8 | 9 | ## 0.1.1 10 | 11 | - No significant changes since `0.1.0`. 12 | 13 | ## 0.1.0 14 | 15 | - No significant changes since `0.0.2`. 16 | 17 | ## 0.0.2 18 | 19 | - No significant changes since `0.0.1`. 20 | 21 | ## 0.0.1 22 | 23 | - Initial release. 24 | -------------------------------------------------------------------------------- /err-report/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "err-report" 3 | version = "0.1.2" 4 | description = "Clone of the unstable `std::error::Report` type" 5 | repository.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | 10 | [dependencies] 11 | 12 | [dev-dependencies] 13 | indoc = "2" 14 | 15 | [lints] 16 | workspace = true 17 | -------------------------------------------------------------------------------- /actix-web-lab-derive/tests/trybuild.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | #[rustversion::stable(1.70)] // MSRV 4 | #[test] 5 | fn compile_macros() { 6 | let t = trybuild::TestCases::new(); 7 | 8 | t.pass("tests/trybuild/ok-no-body-type.rs"); 9 | t.pass("tests/trybuild/ok-with-body-type.rs"); 10 | 11 | t.compile_fail("tests/trybuild/err-invalid-structures.rs"); 12 | } 13 | -------------------------------------------------------------------------------- /russe/src/event.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bytestring::ByteString; 4 | 5 | use crate::message::Message; 6 | 7 | /// An SSE event. 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub enum Event { 10 | /// Message event. 11 | Message(Message), 12 | 13 | /// Comment event. 14 | Comment(ByteString), 15 | 16 | /// Retry recommendation event. 17 | Retry(Duration), 18 | } 19 | -------------------------------------------------------------------------------- /actix-web-lab/src/test.rs: -------------------------------------------------------------------------------- 1 | //! Experimental testing utilities. 2 | 3 | #[doc(inline)] 4 | #[cfg(test)] 5 | pub(crate) use crate::test_header_macros::{header_round_trip_test, header_test_module}; 6 | #[doc(inline)] 7 | pub use crate::test_request_macros::test_request; 8 | #[doc(inline)] 9 | pub use crate::test_response_macros::assert_response_matches; 10 | pub use crate::test_services::echo_path_service; 11 | -------------------------------------------------------------------------------- /collectools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "collectools" 3 | version = "0.1.0" 4 | description = "Collection traits and implementations for common crates" 5 | repository.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | 10 | [dependencies] 11 | arrayvec = "0.7" 12 | smallvec = "1" 13 | tinyvec = "1" 14 | 15 | [lints] 16 | workspace = true 17 | -------------------------------------------------------------------------------- /russe/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## Unreleased 4 | 5 | - Upgrade to edition 2024. 6 | - Minimum supported Rust version (MSRV) is now 1.85. 7 | 8 | ## 0.0.5 9 | 10 | - The `Message::id` field is now an `Option`. 11 | - The `Manager::commit_id()` method now receives an `impl Into`. 12 | - When decoding, split input only on UNIX newlines. 13 | - When decoding, yield errors when input contains invalid UTF-8 instead of panicking. 14 | 15 | ## 0.0.4 16 | 17 | - Initial release. 18 | -------------------------------------------------------------------------------- /collectools/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Collection traits. 2 | 3 | #![allow(missing_docs)] 4 | 5 | mod arrayvec; 6 | mod smallvec; 7 | mod tinyvec; 8 | mod vec; 9 | 10 | pub trait List { 11 | fn len(&self) -> usize; 12 | 13 | fn is_empty(&self) -> bool { 14 | self.len() == 0 15 | } 16 | 17 | fn get(&self, idx: usize) -> Option<&T>; 18 | } 19 | 20 | pub trait MutableList: List { 21 | fn append(&mut self, element: T); 22 | 23 | fn get_mut(&mut self, idx: usize) -> Option<&mut T>; 24 | } 25 | -------------------------------------------------------------------------------- /actix-web-lab/src/middleware.rs: -------------------------------------------------------------------------------- 1 | //! Experimental middleware. 2 | //! 3 | //! Analogous to the `middleware` module in Actix Web. 4 | 5 | pub use crate::{ 6 | catch_panic::CatchPanic, 7 | condition_option::ConditionOption, 8 | err_handler::ErrorHandlers, 9 | load_shed::LoadShed, 10 | middleware_map_response::{MapResMiddleware, map_response}, 11 | middleware_map_response_body::{MapResBodyMiddleware, map_response_body}, 12 | normalize_path::NormalizePath, 13 | panic_reporter::PanicReporter, 14 | redirect_to_https::RedirectHttps, 15 | redirect_to_non_www::redirect_to_non_www, 16 | redirect_to_www::redirect_to_www, 17 | }; 18 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | exclude = ["target/*"] 2 | include = ["**/*.toml"] 3 | 4 | [formatting] 5 | column_width = 110 6 | 7 | [[rule]] 8 | include = ["**/Cargo.toml"] 9 | keys = [ 10 | "dependencies", 11 | "*-dependencies", 12 | "workspace.dependencies", 13 | "workspace.*-dependencies", 14 | "target.*.dependencies", 15 | "target.*.*-dependencies", 16 | ] 17 | formatting.reorder_keys = true 18 | 19 | [[rule]] 20 | include = ["**/Cargo.toml"] 21 | keys = [ 22 | "dependencies.*", 23 | "*-dependencies.*", 24 | "workspace.dependencies.*", 25 | "workspace.*-dependencies.*", 26 | "target.*.dependencies", 27 | "target.*.*-dependencies", 28 | ] 29 | formatting.reorder_keys = false 30 | -------------------------------------------------------------------------------- /actix-hash/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | - Upgrade to edition 2024. 6 | - Minimum supported Rust version (MSRV) is now 1.85. 7 | 8 | ## 0.5.0 9 | 10 | - Add `BodyBlake3` extractor. 11 | - Minimum supported Rust version (MSRV) is now 1.64. 12 | 13 | ## 0.4.0 14 | 15 | - Minimum supported Rust version (MSRV) is now 1.60. 16 | 17 | ## 0.3.0 18 | 19 | - Removed `BodyHashParts::body_bytes` field. 20 | - Rename `BodyHashParts::{body => inner}` field. 21 | - Improve fault tolerance when placed on non-body extractors. 22 | 23 | ## 0.2.0 24 | 25 | - Body hashing extractors for many popular, general purpose hashing algorithms. 26 | 27 | # 0.1.0 28 | 29 | - Empty crate. 30 | -------------------------------------------------------------------------------- /actix-hash/README.md: -------------------------------------------------------------------------------- 1 | # actix-hash 2 | 3 | > Hashing utilities for Actix Web. 4 | 5 | 6 | 7 | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)](https://crates.io/crates/actix-hash) 8 | [![Documentation](https://docs.rs/actix-hash/badge.svg?version=0.5.0)](https://docs.rs/actix-hash/0.5.0) 9 | ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-hash.svg) 10 |
11 | [![dependency status](https://deps.rs/crate/actix-hash/0.5.0/status.svg)](https://deps.rs/crate/actix-hash/0.5.0) 12 | [![Download](https://img.shields.io/crates/d/actix-hash.svg)](https://crates.io/crates/actix-hash) 13 | 14 | 15 | -------------------------------------------------------------------------------- /russe/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, io}; 2 | 3 | /// SSE decoding error. 4 | #[derive(Debug)] 5 | #[non_exhaustive] 6 | pub enum Error { 7 | /// Invalid SSE format. 8 | Invalid, 9 | 10 | /// I/O error. 11 | Io(io::Error), 12 | } 13 | 14 | impl fmt::Display for Error { 15 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 16 | f.write_str(match self { 17 | Error::Invalid => "Invalid SSE format", 18 | Error::Io(_) => "I/O error", 19 | }) 20 | } 21 | } 22 | 23 | impl std::error::Error for Error {} 24 | 25 | impl From for Error { 26 | fn from(err: io::Error) -> Self { 27 | Self::Io(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | - Update `ipnetwork` dependency to `0.21`. 6 | - Upgrade to edition 2024. 7 | - Minimum supported Rust version (MSRV) is now 1.85. 8 | 9 | ## 0.2.0 10 | 11 | - Replace visible types from `cidr-utils` with equivalent types from the `ipnetwork` crate. 12 | 13 | ## 0.1.1 14 | 15 | - Add `TrustedIps::new()` constructor. 16 | - Add `TrustedIps::add_ip_range()` method. 17 | - Add `TrustedIps::{add_loopback_ips, add_private_ips}()` methods. 18 | - Implement `Default` for `TrustedIps`. 19 | - Add `CfConnectingIp[v6]::is_trusted()` method. 20 | - Deprecate `TrustedIps::with_ip_range()` method. 21 | 22 | ## 0.1.0 23 | 24 | - Initial release. 25 | -------------------------------------------------------------------------------- /actix-web-lab/src/test_services.rs: -------------------------------------------------------------------------------- 1 | use actix_utils::future::ok; 2 | use actix_web::{ 3 | Error, HttpResponseBuilder, 4 | body::BoxBody, 5 | dev::{Service, ServiceRequest, ServiceResponse, fn_service}, 6 | http::StatusCode, 7 | }; 8 | 9 | /// Creates service that always responds with given status code and echoes request path as response 10 | /// body. 11 | pub fn echo_path_service( 12 | status_code: StatusCode, 13 | ) -> impl Service, Error = Error> { 14 | fn_service(move |req: ServiceRequest| { 15 | let path = req.path().to_owned(); 16 | ok(req.into_response(HttpResponseBuilder::new(status_code).body(path))) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /actix-web-lab/examples/assets/spa.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Actix Web SPA 8 | 9 | 10 | Actix Web Logo 11 |

Sample SPA

12 |

Text below is loaded from the API

13 |

14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /actix-web-lab-derive/tests/trybuild/err-invalid-structures.stderr: -------------------------------------------------------------------------------- 1 | error: Deriving FromRequest is only supported on structs for now. 2 | --> tests/trybuild/err-invalid-structures.rs:3:10 3 | | 4 | 3 | #[derive(FromRequest)] 5 | | ^^^^^^^^^^^ 6 | | 7 | = note: this error originates in the derive macro `FromRequest` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: Deriving FromRequest is only supported on structs with named fields for now. 10 | --> tests/trybuild/err-invalid-structures.rs:8:10 11 | | 12 | 8 | #[derive(FromRequest)] 13 | | ^^^^^^^^^^^ 14 | | 15 | = note: this error originates in the derive macro `FromRequest` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/examples/fetch-ips.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates obtaining the trusted set of CloudFlare IPs. 2 | 3 | use std::net::IpAddr; 4 | 5 | use actix_client_ip_cloudflare::fetch_trusted_cf_ips; 6 | 7 | #[actix_web::main] 8 | async fn main() { 9 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 10 | 11 | let ips = fetch_trusted_cf_ips().await.unwrap(); 12 | 13 | dbg!(ips.contains(IpAddr::from([103, 21, 243, 0]))); 14 | dbg!(ips.contains(IpAddr::from([103, 21, 244, 0]))); 15 | dbg!(ips.contains(IpAddr::from([103, 21, 245, 0]))); 16 | dbg!(ips.contains(IpAddr::from([103, 21, 246, 0]))); 17 | dbg!(ips.contains(IpAddr::from([103, 21, 247, 0]))); 18 | dbg!(ips.contains(IpAddr::from([103, 21, 248, 0]))); 19 | } 20 | -------------------------------------------------------------------------------- /.cspell.yml: -------------------------------------------------------------------------------- 1 | version: "0.2" 2 | words: 3 | - actix 4 | - ahash 5 | - arrayvec 6 | - bytestring 7 | - cbor 8 | - clippy 9 | - codecov 10 | - collectools 11 | - corasick 12 | - dalek 13 | - deque 14 | - deserialization 15 | - docsrs 16 | - dtolnay 17 | - Expiremental 18 | - hmac 19 | - hsts 20 | - indoc 21 | - ipnetwork 22 | - itertools 23 | - memchr 24 | - memmem 25 | - msgpack 26 | - msrv 27 | - ndjson 28 | - nextest 29 | - nixpkgs 30 | - pemfile 31 | - pkgs 32 | - prehashed 33 | - rdef 34 | - refcell 35 | - reqwest 36 | - robjtede 37 | - rustdoc 38 | - RUSTDOCFLAGS 39 | - rustfmt 40 | - rustls 41 | - rustup 42 | - rustversion 43 | - serde 44 | - smallvec 45 | - splitn 46 | - struct 47 | - taplo 48 | - tinyvec 49 | - trybuild 50 | - vals 51 | -------------------------------------------------------------------------------- /actix-web-lab/src/web.rs: -------------------------------------------------------------------------------- 1 | //! Experimental services. 2 | //! 3 | //! Analogous to the `web` module in Actix Web. 4 | 5 | #[cfg(feature = "spa")] 6 | pub use crate::spa::Spa; 7 | 8 | /// Constructs a new Single-page Application (SPA) builder. 9 | /// 10 | /// See [`Spa`] docs for more details. 11 | /// 12 | /// # Examples 13 | /// ``` 14 | /// # use actix_web::App; 15 | /// # use actix_web_lab::web::spa; 16 | /// let app = App::new() 17 | /// // ...api routes... 18 | /// .service( 19 | /// spa() 20 | /// .index_file("./examples/assets/spa.html") 21 | /// .static_resources_mount("/static") 22 | /// .static_resources_location("./examples/assets") 23 | /// .finish(), 24 | /// ); 25 | /// ``` 26 | #[cfg(feature = "spa")] 27 | pub fn spa() -> Spa { 28 | Spa::default() 29 | } 30 | -------------------------------------------------------------------------------- /actix-web-lab/src/extract.rs: -------------------------------------------------------------------------------- 1 | //! Experimental extractors. 2 | 3 | /// An alias for [`actix_web::web::Data`] with a more descriptive name. 4 | pub type SharedData = actix_web::web::Data; 5 | 6 | pub use crate::{ 7 | body_limit::{BodyLimit, DEFAULT_BODY_LIMIT}, 8 | bytes::{Bytes, DEFAULT_BYTES_LIMIT}, 9 | host::Host, 10 | json::{DEFAULT_JSON_LIMIT, Json, JsonDeserializeError, JsonPayloadError}, 11 | lazy_data::LazyData, 12 | lazy_data_shared::LazyDataShared, 13 | local_data::LocalData, 14 | path::Path, 15 | query::{Query, QueryDeserializeError}, 16 | request_signature::{RequestSignature, RequestSignatureError, RequestSignatureScheme}, 17 | swap_data::SwapData, 18 | url_encoded_form::{ 19 | DEFAULT_URL_ENCODED_FORM_LIMIT, UrlEncodedForm, UrlEncodedFormDeserializeError, 20 | }, 21 | x_forwarded_prefix::ReconstructedPath, 22 | }; 23 | -------------------------------------------------------------------------------- /russe/README.md: -------------------------------------------------------------------------------- 1 | # russe 2 | 3 | 4 | 5 | [![crates.io](https://img.shields.io/crates/v/russe?label=latest)](https://crates.io/crates/russe) 6 | [![Documentation](https://docs.rs/russe/badge.svg?version=0.0.5)](https://docs.rs/russe/0.0.5) 7 | ![Version](https://img.shields.io/badge/rustc-1.75+-ab6000.svg) 8 | ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/russe.svg) 9 |
10 | [![Dependency Status](https://deps.rs/crate/russe/0.0.5/status.svg)](https://deps.rs/crate/russe/0.0.5) 11 | [![Download](https://img.shields.io/crates/d/russe.svg)](https://crates.io/crates/russe) 12 | [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) 13 | 14 | 15 | 16 | 17 | 18 | Server-Sent Events (SSE) decoder. 19 | 20 | 21 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; 4 | flake-parts.url = "github:hercules-ci/flake-parts"; 5 | }; 6 | 7 | outputs = inputs@{ flake-parts, ... }: 8 | flake-parts.lib.mkFlake { inherit inputs; } { 9 | systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 10 | perSystem = { pkgs, config, inputs', system, lib, ... }: { 11 | formatter = pkgs.nixpkgs-fmt; 12 | 13 | devShells.default = pkgs.mkShell { 14 | packages = [ 15 | config.formatter 16 | pkgs.cargo-rdme 17 | pkgs.fd 18 | pkgs.just 19 | pkgs.nodePackages.prettier 20 | pkgs.taplo 21 | pkgs.watchexec 22 | ] ++ lib.optional pkgs.stdenv.isDarwin [ 23 | pkgs.pkgsBuildHost.libiconv 24 | ]; 25 | }; 26 | }; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /actix-web-lab/examples/cbor.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates use of the CBOR responder. 2 | 3 | use std::io; 4 | 5 | use actix_web::{App, HttpServer, Responder, get}; 6 | use actix_web_lab::respond::Cbor; 7 | use serde::Serialize; 8 | use tracing::info; 9 | 10 | #[derive(Debug, Serialize)] 11 | struct Test { 12 | one: u32, 13 | two: String, 14 | } 15 | 16 | #[get("/")] 17 | async fn index() -> impl Responder { 18 | Cbor(Test { 19 | one: 42, 20 | two: "two".to_owned(), 21 | }) 22 | } 23 | 24 | #[actix_web::main] 25 | async fn main() -> io::Result<()> { 26 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 27 | 28 | let bind = ("127.0.0.1", 8080); 29 | info!("staring server at http://{}:{}", &bind.0, &bind.1); 30 | 31 | HttpServer::new(|| App::new().service(index)) 32 | .workers(1) 33 | .bind(bind)? 34 | .run() 35 | .await 36 | } 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "actix-client-ip-cloudflare", 5 | "actix-hash", 6 | "actix-web-lab-derive", 7 | "actix-web-lab", 8 | "actix-proxy-protocol", 9 | "collectools", 10 | "err-report", 11 | "russe", 12 | ] 13 | 14 | [workspace.package] 15 | repository = "https://github.com/robjtede/actix-web-lab" 16 | license = "MIT OR Apache-2.0" 17 | edition = "2024" 18 | rust-version = "1.85" 19 | 20 | [workspace.lints.rust] 21 | rust_2018_idioms = { level = "deny", priority = 10 } 22 | nonstandard_style = { level = "deny", priority = 5 } 23 | future_incompatible = "warn" 24 | missing_docs = "warn" 25 | missing_debug_implementations = "warn" 26 | 27 | [patch.crates-io] 28 | actix-client-ip-cloudflare = { path = "./actix-client-ip-cloudflare" } 29 | actix-hash = { path = "./actix-hash" } 30 | actix-web-lab = { path = "./actix-web-lab" } 31 | actix-web-lab-derive = { path = "./actix-web-lab-derive" } 32 | -------------------------------------------------------------------------------- /actix-web-lab/src/cbor.rs: -------------------------------------------------------------------------------- 1 | //! CBOR responder. 2 | 3 | use std::sync::LazyLock; 4 | 5 | use actix_web::{HttpRequest, HttpResponse, Responder}; 6 | use bytes::Bytes; 7 | use derive_more::Display; 8 | use mime::Mime; 9 | use serde::Serialize; 10 | 11 | static CBOR_MIME: LazyLock = LazyLock::new(|| "application/cbor".parse().unwrap()); 12 | 13 | /// [CBOR] responder. 14 | /// 15 | /// [CBOR]: https://cbor.io/ 16 | #[derive(Debug, Display)] 17 | pub struct Cbor(pub T); 18 | 19 | impl_more::impl_deref_and_mut!( in Cbor => T); 20 | 21 | impl Responder for Cbor { 22 | type Body = Bytes; 23 | 24 | fn respond_to(self, _req: &HttpRequest) -> HttpResponse { 25 | let body = Bytes::from(serde_cbor_2::to_vec(&self.0).unwrap()); 26 | 27 | HttpResponse::Ok() 28 | .content_type(CBOR_MIME.clone()) 29 | .message_body(body) 30 | .unwrap() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /actix-web-lab/src/infallible_body_stream.rs: -------------------------------------------------------------------------------- 1 | use actix_http::body::{BodyStream, SizedStream}; 2 | use bytes::Bytes; 3 | use futures_core::Stream; 4 | 5 | use crate::util::InfallibleStream; 6 | 7 | /// Constructs a new [`BodyStream`] from an infallible byte chunk stream. 8 | /// 9 | /// This could be stabilized into Actix Web as `BodyStream::from_infallible()`. 10 | pub fn new_infallible_body_stream>( 11 | stream: S, 12 | ) -> BodyStream> { 13 | BodyStream::new(InfallibleStream::new(stream)) 14 | } 15 | 16 | /// Constructs a new [`SizedStream`] from an infallible byte chunk stream. 17 | /// 18 | /// This could be stabilized into Actix Web as `SizedStream::from_infallible()`. 19 | pub fn new_infallible_sized_stream>( 20 | size: u64, 21 | stream: S, 22 | ) -> SizedStream> { 23 | SizedStream::new(size, InfallibleStream::new(stream)) 24 | } 25 | -------------------------------------------------------------------------------- /actix-hash/examples/body_sha2.rs: -------------------------------------------------------------------------------- 1 | //! Body + checksum hash extractor usage. 2 | //! 3 | //! For example, sending an empty body will return the hash starting with "E3": 4 | //! ```sh 5 | //! $ curl -XPOST localhost:8080 6 | //! [E3, B0, C4, 42, 98, FC, 1C, ... 7 | //! ``` 8 | 9 | use std::io; 10 | 11 | use actix_hash::BodySha256; 12 | use actix_web::{App, HttpServer, middleware::Logger, web}; 13 | use tracing::info; 14 | 15 | #[actix_web::main] 16 | async fn main() -> io::Result<()> { 17 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 18 | 19 | info!("staring server at http://localhost:8080"); 20 | 21 | HttpServer::new(|| { 22 | App::new().wrap(Logger::default().log_target("@")).route( 23 | "/", 24 | web::post().to(|body: BodySha256| async move { format!("{:X?}", body.hash()) }), 25 | ) 26 | }) 27 | .workers(1) 28 | .bind(("127.0.0.1", 8080))? 29 | .run() 30 | .await 31 | } 32 | -------------------------------------------------------------------------------- /collectools/src/vec.rs: -------------------------------------------------------------------------------- 1 | use super::{List, MutableList}; 2 | 3 | impl List for Vec { 4 | fn len(&self) -> usize { 5 | Vec::len(self) 6 | } 7 | 8 | fn get(&self, idx: usize) -> Option<&T> { 9 | <[_]>::get(self, idx) 10 | } 11 | } 12 | 13 | impl MutableList for Vec { 14 | fn append(&mut self, element: T) { 15 | self.push(element); 16 | } 17 | 18 | fn get_mut(&mut self, idx: usize) -> Option<&mut T> { 19 | <[_]>::get_mut(self, idx) 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | pub(crate) mod tests { 25 | use super::*; 26 | 27 | #[test] 28 | fn it_works() { 29 | let mut vec = vec![]; 30 | 31 | assert_eq!(0, List::len(&vec)); 32 | assert!(List::is_empty(&vec)); 33 | 34 | MutableList::append(&mut vec, 1); 35 | 36 | assert_eq!(1, List::len(&vec)); 37 | assert!(!List::is_empty(&vec)); 38 | 39 | assert_eq!(&1, List::get(&vec, 0).unwrap()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /actix-web-lab-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-lab-derive" 3 | version = "0.24.0" 4 | description = "Experimental macros for Actix Web" 5 | authors = ["Rob Ede "] 6 | keywords = ["actix", "http", "web", "framework", "async"] 7 | categories = [ 8 | "network-programming", 9 | "asynchronous", 10 | "web-programming::http-server", 11 | "web-programming::websocket", 12 | ] 13 | repository.workspace = true 14 | license.workspace = true 15 | edition.workspace = true 16 | rust-version.workspace = true 17 | 18 | [lib] 19 | proc-macro = true 20 | 21 | [dependencies] 22 | quote = "1" 23 | syn = { version = "2", features = ["full", "parsing"] } 24 | 25 | [dev-dependencies] 26 | actix-web-lab = "0.24" 27 | 28 | actix-test = "0.1" 29 | actix-web = "4" 30 | futures-util = { version = "0.3.31", default-features = false, features = ["std"] } 31 | rustversion = "1" 32 | tokio = { version = "1.38.2", features = ["macros"] } 33 | trybuild = "1" 34 | 35 | [lints] 36 | workspace = true 37 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: read 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | coverage: 16 | name: Coverage 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v5 20 | 21 | - name: Install Rust 22 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 23 | with: 24 | components: llvm-tools 25 | 26 | - name: Install just, cargo-llvm-cov 27 | uses: taiki-e/install-action@v2.62.44 28 | with: 29 | tool: just,cargo-llvm-cov 30 | 31 | - name: Generate code coverage 32 | run: just test-coverage-codecov 33 | 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v5.5.1 36 | with: 37 | files: codecov.json 38 | fail_ci_if_error: true 39 | env: 40 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 41 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/README.md: -------------------------------------------------------------------------------- 1 | # actix-client-ip-cloudflare 2 | 3 | > Extractor for trustworthy client IP addresses when proxied through Cloudflare 4 | 5 | 6 | 7 | [![crates.io](https://img.shields.io/crates/v/actix-client-ip-cloudflare?label=latest)](https://crates.io/crates/actix-client-ip-cloudflare) 8 | [![Documentation](https://docs.rs/actix-client-ip-cloudflare/badge.svg?version=0.2.0)](https://docs.rs/actix-client-ip-cloudflare/0.2.0) 9 | ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-client-ip-cloudflare.svg) 10 |
11 | [![dependency status](https://deps.rs/crate/actix-client-ip-cloudflare/0.2.0/status.svg)](https://deps.rs/crate/actix-client-ip-cloudflare/0.2.0) 12 | [![Download](https://img.shields.io/crates/d/actix-client-ip-cloudflare.svg)](https://crates.io/crates/actix-client-ip-cloudflare) 13 | 14 | 15 | 16 | See for more documentation on the headers interpreted by this crate. 17 | -------------------------------------------------------------------------------- /actix-proxy-protocol/README.md: -------------------------------------------------------------------------------- 1 | # actix-proxy-protocol 2 | 3 | > Implementation of the [PROXY protocol]. 4 | 5 | 6 | 7 | [![crates.io](https://img.shields.io/crates/v/actix-proxy-protocol?label=latest)](https://crates.io/crates/actix-proxy-protocol) 8 | [![Documentation](https://docs.rs/actix-proxy-protocol/badge.svg?version=0.0.2)](https://docs.rs/actix-proxy-protocol/0.0.2) 9 | ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-proxy-protocol.svg) 10 |
11 | [![dependency status](https://deps.rs/crate/actix-proxy-protocol/0.0.2/status.svg)](https://deps.rs/crate/actix-proxy-protocol/0.0.2) 12 | [![Download](https://img.shields.io/crates/d/actix-proxy-protocol.svg)](https://crates.io/crates/actix-proxy-protocol) 13 | [![codecov](https://codecov.io/gh/robjtede/actix-proxy-protocol/branch/main/graph/badge.svg)](https://codecov.io/gh/robjtede/actix-proxy-protocol) 14 | 15 | 16 | 17 | ## Resources 18 | 19 | - [Examples](./examples) 20 | 21 | [proxy protocol]: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt 22 | -------------------------------------------------------------------------------- /collectools/src/arrayvec.rs: -------------------------------------------------------------------------------- 1 | use arrayvec::ArrayVec; 2 | 3 | use super::{List, MutableList}; 4 | 5 | impl List for ArrayVec { 6 | fn len(&self) -> usize { 7 | ArrayVec::len(self) 8 | } 9 | 10 | fn get(&self, idx: usize) -> Option<&T> { 11 | <[_]>::get(self, idx) 12 | } 13 | } 14 | 15 | impl MutableList for ArrayVec { 16 | fn append(&mut self, element: T) { 17 | self.push(element); 18 | } 19 | 20 | fn get_mut(&mut self, idx: usize) -> Option<&mut T> { 21 | <[_]>::get_mut(self, idx) 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | pub(crate) mod tests { 27 | use super::*; 28 | 29 | #[test] 30 | fn it_works() { 31 | let mut vec = ArrayVec::<_, 8>::new(); 32 | 33 | assert_eq!(0, List::len(&vec)); 34 | assert!(List::is_empty(&vec)); 35 | 36 | MutableList::append(&mut vec, 1); 37 | 38 | assert_eq!(1, List::len(&vec)); 39 | assert!(!List::is_empty(&vec)); 40 | 41 | assert_eq!(&1, List::get(&vec, 0).unwrap()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /collectools/src/tinyvec.rs: -------------------------------------------------------------------------------- 1 | use tinyvec::{Array, ArrayVec}; 2 | 3 | use super::{List, MutableList}; 4 | 5 | impl List for ArrayVec { 6 | fn len(&self) -> usize { 7 | ArrayVec::len(self) 8 | } 9 | 10 | fn get(&self, idx: usize) -> Option<&A::Item> { 11 | <[_]>::get(self, idx) 12 | } 13 | } 14 | 15 | impl MutableList for ArrayVec { 16 | fn append(&mut self, element: A::Item) { 17 | self.push(element); 18 | } 19 | 20 | fn get_mut(&mut self, idx: usize) -> Option<&mut A::Item> { 21 | <[_]>::get_mut(self, idx) 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | pub(crate) mod tests { 27 | use super::*; 28 | 29 | #[test] 30 | fn it_works() { 31 | let mut vec = ArrayVec::<[_; 8]>::new(); 32 | 33 | assert_eq!(0, List::len(&vec)); 34 | assert!(List::is_empty(&vec)); 35 | 36 | MutableList::append(&mut vec, 1); 37 | 38 | assert_eq!(1, List::len(&vec)); 39 | assert!(!List::is_empty(&vec)); 40 | 41 | assert_eq!(&1, List::get(&vec, 0).unwrap()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /collectools/src/smallvec.rs: -------------------------------------------------------------------------------- 1 | use smallvec::{Array, SmallVec}; 2 | 3 | use super::{List, MutableList}; 4 | 5 | impl List for SmallVec { 6 | fn len(&self) -> usize { 7 | SmallVec::len(self) 8 | } 9 | 10 | fn get(&self, idx: usize) -> Option<&A::Item> { 11 | <[_]>::get(self, idx) 12 | } 13 | } 14 | 15 | impl MutableList for SmallVec { 16 | fn append(&mut self, element: A::Item) { 17 | self.push(element); 18 | } 19 | 20 | fn get_mut(&mut self, idx: usize) -> Option<&mut A::Item> { 21 | <[_]>::get_mut(self, idx) 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | pub(crate) mod tests { 27 | use super::*; 28 | 29 | #[test] 30 | fn it_works() { 31 | let mut vec = SmallVec::<[_; 8]>::new(); 32 | 33 | assert_eq!(0, List::len(&vec)); 34 | assert!(List::is_empty(&vec)); 35 | 36 | MutableList::append(&mut vec, 1); 37 | 38 | assert_eq!(1, List::len(&vec)); 39 | assert!(!List::is_empty(&vec)); 40 | 41 | assert_eq!(&1, List::get(&vec, 0).unwrap()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /poll-results.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -euo pipefail 4 | 5 | F="$(mktemp)" 6 | 7 | HAS_XSV="$(command -v xsv)" 8 | # HAS_XSV="" 9 | 10 | if [ ! "$(command -v gh)" ]; then 11 | echo "This script requires the GitHub CLI." 12 | echo "https://cli.github.com" 13 | exit 1 14 | fi 15 | 16 | 17 | if [ "$HAS_XSV" ]; then 18 | echo 'votes,feature,"issue url"' >> "$F" 19 | else 20 | echo "votes \tfeature \tissue url" >> "$F" 21 | fi 22 | 23 | gh issue list \ 24 | --repo="robjtede/actix-web-lab" \ 25 | --limit=999 \ 26 | --search="is:issue is:open label:poll sort:reactions-+1-desc" \ 27 | --json="title,url,reactionGroups" \ 28 | --jq ' 29 | .[] 30 | | { 31 | title, 32 | url, 33 | votes: ((.reactionGroups[]? | select(.content == "THUMBS_UP") | .users.totalCount) // 0) 34 | } 35 | | "\(.votes),\"\(.title)\",\(.url)" 36 | ' \ 37 | | sed -E 's/(.*)\[poll\] (.*)/\1\2/' >> "$F" 38 | 39 | if [ "$HAS_XSV" ]; then 40 | cat "$F" | xsv table 41 | else 42 | cat "$F" | awk -F "\"*,\"*" '{ print $1 "\t" $2 "\t" $3 }' 43 | fi 44 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-NOW Rob Ede 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /russe/examples/manager.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates usage of the SSE connection manager. 2 | 3 | extern crate reqwest_0_12 as reqwest; 4 | 5 | use futures_util::StreamExt as _; 6 | use reqwest::{Method, Request}; 7 | use russe::reqwest_0_12::Manager; 8 | use tokio_stream::wrappers::UnboundedReceiverStream; 9 | 10 | #[tokio::main(flavor = "current_thread")] 11 | async fn main() -> eyre::Result<()> { 12 | color_eyre::install()?; 13 | 14 | let client = reqwest::Client::default(); 15 | 16 | let mut req = Request::new(Method::GET, "https://sse.dev/test".parse().unwrap()); 17 | let headers = req.headers_mut(); 18 | headers.insert("accept", russe::MEDIA_TYPE_STR.parse().unwrap()); 19 | 20 | let mut manager = Manager::new(&client, req); 21 | 22 | let (_task_handle, events) = manager.send().await.unwrap(); 23 | 24 | let mut event_stream = UnboundedReceiverStream::new(events); 25 | 26 | while let Some(Ok(ev)) = event_stream.next().await { 27 | println!("{ev:?}"); 28 | 29 | if let russe::Event::Message(msg) = ev { 30 | if let Some(id) = msg.id { 31 | manager.commit_id(id); 32 | } 33 | } 34 | } 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /russe/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Server-Sent Events (SSE) decoder. 2 | 3 | #![forbid(unsafe_code)] 4 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 5 | 6 | mod decoder; 7 | mod encoder; 8 | mod error; 9 | mod event; 10 | mod message; 11 | #[cfg(feature = "reqwest-0_12")] 12 | pub mod reqwest_0_12; 13 | mod unix_lines; 14 | 15 | pub use self::{decoder::Decoder, error::Error, event::Event, message::Message}; 16 | 17 | /// A specialized `Result` type for `russe` operations. 18 | pub type Result = std::result::Result; 19 | 20 | pub(crate) const NEWLINE: u8 = b'\n'; 21 | pub(crate) const SSE_DELIMITER: &[u8] = b"\n\n"; 22 | 23 | /// Media (MIME) type for SSE (`text/event-stream`). 24 | #[cfg(feature = "mime")] 25 | pub const MEDIA_TYPE: mime::Mime = mime::TEXT_EVENT_STREAM; 26 | 27 | /// Media (MIME) type for SSE (`text/event-stream`). 28 | pub const MEDIA_TYPE_STR: &str = "text/event-stream"; 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | /// Asserts that `Option` argument is `None`. 33 | #[macro_export] 34 | macro_rules! assert_none { 35 | ($exp:expr) => {{ 36 | let exp = $exp; 37 | assert!(exp.is_none(), "Expected None; got: {exp:?}"); 38 | }}; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /actix-web-lab/examples/msgpack.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates use of the MessagePack responder. 2 | 3 | use std::io; 4 | 5 | use actix_web::{App, HttpServer, Responder, get, http::StatusCode}; 6 | use actix_web_lab::respond::{MessagePack, MessagePackNamed}; 7 | use serde::Serialize; 8 | use tracing::info; 9 | 10 | #[derive(Debug, Serialize)] 11 | struct Test { 12 | one: u32, 13 | two: String, 14 | } 15 | 16 | #[get("/")] 17 | async fn index() -> impl Responder { 18 | MessagePack(Test { 19 | one: 42, 20 | two: "two".to_owned(), 21 | }) 22 | } 23 | 24 | #[get("/named")] 25 | async fn named() -> impl Responder { 26 | MessagePackNamed(Test { 27 | one: 42, 28 | two: "two".to_owned(), 29 | }) 30 | .customize() 31 | .with_status(StatusCode::CREATED) 32 | } 33 | 34 | #[actix_web::main] 35 | async fn main() -> io::Result<()> { 36 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 37 | 38 | let bind = ("127.0.0.1", 8080); 39 | info!("staring server at http://{}:{}", &bind.0, &bind.1); 40 | 41 | HttpServer::new(|| App::new().service(index).service(named)) 42 | .workers(1) 43 | .bind(bind)? 44 | .run() 45 | .await 46 | } 47 | -------------------------------------------------------------------------------- /russe/src/message.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bytestring::ByteString; 4 | 5 | /// An SSE data message. 6 | #[derive(Debug, Clone, PartialEq)] 7 | pub struct Message { 8 | /// Message data. 9 | pub data: ByteString, 10 | 11 | /// Name of event. 12 | pub event: Option, 13 | 14 | /// Recommended retry delay in milliseconds. 15 | pub retry: Option, 16 | 17 | /// Event identifier. 18 | /// 19 | /// Used in Last-Event-ID header. 20 | /// 21 | /// See . 22 | pub id: Option, 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | 29 | impl Message { 30 | pub(crate) fn data(data: impl Into) -> Self { 31 | Self { 32 | data: data.into(), 33 | ..Default::default() 34 | } 35 | } 36 | } 37 | 38 | // simplifies some tests 39 | #[allow(clippy::derivable_impls)] 40 | impl Default for Message { 41 | fn default() -> Self { 42 | Self { 43 | retry: None, 44 | event: None, 45 | data: ByteString::new(), 46 | id: None, 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/examples/extract-header.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates use of the client IP header extractor. 2 | 3 | use actix_client_ip_cloudflare::{CfConnectingIp, TrustedClientIp, fetch_trusted_cf_ips}; 4 | use actix_web::{App, HttpServer, Responder, get, web::Header}; 5 | 6 | #[get("/raw-header")] 7 | async fn header(Header(client_ip): Header) -> impl Responder { 8 | match client_ip { 9 | CfConnectingIp::Trusted(_ip) => unreachable!(), 10 | CfConnectingIp::Untrusted(ip) => format!("Possibly fake client IP: {ip}"), 11 | } 12 | } 13 | 14 | #[get("/client-ip")] 15 | async fn trusted_client_ip(client_ip: TrustedClientIp) -> impl Responder { 16 | format!("Trusted client IP: {client_ip}") 17 | } 18 | 19 | #[actix_web::main] 20 | async fn main() -> std::io::Result<()> { 21 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 22 | 23 | let cloudflare_ips = fetch_trusted_cf_ips() 24 | .await 25 | .unwrap() 26 | .add_ip_range("127.0.0.1/24".parse().unwrap()); 27 | 28 | HttpServer::new(move || { 29 | App::new() 30 | .app_data(cloudflare_ips.clone()) 31 | .service(header) 32 | .service(trusted_client_ip) 33 | }) 34 | .bind(("127.0.0.1", 8080))? 35 | .workers(2) 36 | .run() 37 | .await 38 | } 39 | -------------------------------------------------------------------------------- /actix-web-lab/examples/spa.rs: -------------------------------------------------------------------------------- 1 | //! Simple builder for a SPA (Single Page Application) service builder. 2 | 3 | use std::io; 4 | 5 | use actix_web::{App, HttpServer, middleware::Logger, web}; 6 | use actix_web_lab::web::spa; 7 | use tracing::info; 8 | 9 | #[actix_web::main] 10 | async fn main() -> io::Result<()> { 11 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 12 | 13 | let bind = ("127.0.0.1", 8080); 14 | info!("staring server at http://{}:{}", &bind.0, &bind.1); 15 | 16 | HttpServer::new(|| { 17 | App::new() 18 | .wrap(Logger::default().log_target("@")) 19 | .route( 20 | "/api/greet", 21 | web::to(|| async { 22 | if rand::random() { 23 | "Hello World!" 24 | } else { 25 | "Greetings, World!" 26 | } 27 | }), 28 | ) 29 | .service( 30 | spa() 31 | .index_file("./examples/assets/spa.html") 32 | .static_resources_mount("/static") 33 | .static_resources_location("./examples/assets") 34 | .finish(), 35 | ) 36 | }) 37 | .workers(1) 38 | .bind(bind)? 39 | .run() 40 | .await 41 | } 42 | -------------------------------------------------------------------------------- /russe/src/unix_lines.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use bytes::Bytes; 4 | 5 | /// A lines iterator that only splits on `\n`. 6 | #[derive(Debug)] 7 | pub(crate) struct UnixLines { 8 | pub(crate) rdr: R, 9 | } 10 | 11 | impl Iterator for UnixLines { 12 | type Item = io::Result; 13 | 14 | fn next(&mut self) -> Option { 15 | let mut buf = Vec::new(); 16 | 17 | match self.rdr.read_until(b'\n', &mut buf) { 18 | Ok(0) => None, 19 | Ok(_n) => { 20 | if buf.ends_with(b"\n") { 21 | buf.pop(); 22 | } 23 | 24 | Some(Ok(Bytes::from(buf))) 25 | } 26 | Err(err) => Some(Err(err)), 27 | } 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use super::*; 34 | 35 | #[test] 36 | fn lines() { 37 | let buf = io::Cursor::new(&b"12\r"[..]); 38 | let mut lines = UnixLines { rdr: buf }; 39 | assert_eq!(lines.next().unwrap().unwrap(), "12\r"); 40 | assert!(lines.next().is_none()); 41 | 42 | let buf = io::Cursor::new(&b"12\r\n\n"[..]); 43 | let mut lines = UnixLines { rdr: buf }; 44 | assert_eq!(lines.next().unwrap().unwrap(), "12\r"); 45 | assert_eq!(lines.next().unwrap().unwrap(), ""); 46 | assert!(lines.next().is_none()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /actix-proxy-protocol/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-proxy-protocol" 3 | version = "0.0.2" 4 | authors = ["Rob Ede "] 5 | description = "PROXY protocol utilities" 6 | keywords = ["proxy", "protocol", "network", "haproxy", "tcp"] 7 | categories = ["network-programming", "asynchronous"] 8 | repository.workspace = true 9 | license.workspace = true 10 | edition.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | actix-rt = "2.11" 15 | actix-service = "2" 16 | actix-utils = "3" 17 | arrayvec = "0.7" 18 | bitflags = "2" 19 | crc32fast = "1" 20 | futures-core = { version = "0.3.17", default-features = false, features = ["std"] } 21 | futures-util = { version = "0.3.17", default-features = false, features = ["std"] } 22 | impl-more = "0.3" 23 | itoa = "1" 24 | nom = "8" 25 | pin-project-lite = "0.2" 26 | smallvec = "1" 27 | tokio = { version = "1.13.1", features = ["sync", "io-util"] } 28 | tracing = { version = "0.1.30", default-features = false, features = ["log"] } 29 | 30 | [dev-dependencies] 31 | actix-codec = "0.5" 32 | actix-rt = "2.6" 33 | actix-server = "2" 34 | bytes = "1" 35 | const-str = "0.6" 36 | futures-util = { version = "0.3.7", default-features = false, features = ["sink", "async-await-macro"] } 37 | hex = "0.4" 38 | once_cell = "1" 39 | pretty_assertions = "1" 40 | tokio = { version = "1.38.2", features = ["io-util", "rt-multi-thread", "macros", "fs"] } 41 | tracing-subscriber = "0.3" 42 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | permissions: 8 | contents: read 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | fmt: 16 | name: Format 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v5 20 | 21 | - name: Install Rust (nightly) 22 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 23 | with: 24 | toolchain: nightly 25 | components: rustfmt 26 | 27 | - name: Check with Rustfmt 28 | run: cargo fmt --all -- --check 29 | 30 | clippy: 31 | name: Clippy 32 | 33 | permissions: 34 | contents: read 35 | checks: write # to add clippy checks to PR diffs 36 | 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v5 40 | 41 | - name: Install Rust 42 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 43 | with: 44 | components: clippy 45 | 46 | - name: Check with Clippy 47 | uses: giraffate/clippy-action@v1.0.1 48 | with: 49 | reporter: github-pr-check 50 | github_token: ${{ secrets.GITHUB_TOKEN }} 51 | clippy_flags: >- 52 | --workspace --all-features --tests --examples --bins -- 53 | -A unknown_lints -D clippy::todo -D clippy::dbg_macro 54 | -------------------------------------------------------------------------------- /actix-web-lab/examples/assets/sse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Server-sent events 8 | 14 | 15 | 16 | 17 |
18 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /russe/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "russe" 3 | version = "0.0.5" 4 | authors = ["Rob Ede "] 5 | description = "Server-Sent Events (SSE) decoder" 6 | keywords = ["sse", "server", "sent", "events"] 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | rust-version.workspace = true 11 | 12 | [package.metadata.docs.rs] 13 | rustdoc-args = ["--cfg", "docsrs"] 14 | all-features = true 15 | 16 | [features] 17 | awc-3 = [] 18 | mime = ["dep:mime"] 19 | reqwest-0_12 = ["dep:reqwest-0_12"] 20 | 21 | [dependencies] 22 | aho-corasick = "1" 23 | bytes = "1" 24 | bytestring = "1.4" 25 | futures-util = "0.3.18" 26 | memchr = "2" 27 | mime = { version = "0.3.17", optional = true } 28 | reqwest-0_12 = { package = "reqwest", version = "0.12", optional = true, features = ["stream"] } 29 | tokio = { version = "1", features = ["sync"] } 30 | tokio-util = { version = "0.7", features = ["codec"] } 31 | tracing = "0.1.41" 32 | 33 | [dev-dependencies] 34 | color-eyre = "0.6" 35 | divan = "0.1" 36 | eyre = "0.6" 37 | futures-test = "0.3" 38 | indoc = "2" 39 | tokio = { version = "1.38.2", features = ["macros", "rt"] } 40 | tokio-stream = "0.1" 41 | tokio-test = "0.4" 42 | tokio-util = { version = "0.7", features = ["codec", "io", "rt"] } 43 | 44 | [[example]] 45 | name = "manager" 46 | required-features = ["reqwest-0_12"] 47 | 48 | [[bench]] 49 | name = "decoder" 50 | harness = false 51 | 52 | [lints] 53 | workspace = true 54 | -------------------------------------------------------------------------------- /russe/examples/decoder.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates usage of the codec to parse a response from a response stream. 2 | 3 | use std::{io, pin::pin, time::Duration}; 4 | 5 | use bytes::Bytes; 6 | use futures_util::{Stream, StreamExt as _}; 7 | use russe::Decoder as SseDecoder; 8 | use tokio_util::{codec::FramedRead, io::StreamReader}; 9 | 10 | #[tokio::main(flavor = "current_thread")] 11 | async fn main() { 12 | let body_reader = StreamReader::new(chunk_stream()); 13 | 14 | let event_stream = FramedRead::new(body_reader, SseDecoder::default()); 15 | let mut event_stream = pin!(event_stream); 16 | 17 | while let Some(Ok(ev)) = event_stream.next().await { 18 | println!("{ev:?}"); 19 | } 20 | } 21 | 22 | fn chunk_stream() -> impl Stream> { 23 | use tokio_test::stream_mock::StreamMockBuilder; 24 | 25 | let input = indoc::indoc! {" 26 | event: add 27 | data: foo 28 | id: 1 29 | 30 | : keep-alive 31 | 32 | event: remove 33 | data: bar 34 | id: 2 35 | 36 | "}; 37 | 38 | let mut mock = StreamMockBuilder::new(); 39 | 40 | // emulate chunked transfer and small delays between chunks 41 | for chunk in input.as_bytes().chunks(7) { 42 | mock = mock.next(Bytes::from(chunk)); 43 | mock = mock.wait(Duration::from_millis(80)); 44 | } 45 | 46 | mock.build().map(Ok) 47 | } 48 | -------------------------------------------------------------------------------- /actix-web-lab-derive/tests/tdd.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use actix_web::{ 4 | App, HttpResponse, Responder, 5 | http::{Method, StatusCode}, 6 | web, 7 | }; 8 | use actix_web_lab_derive::FromRequest; 9 | 10 | #[derive(Debug, FromRequest)] 11 | struct RequestParts { 12 | method: Method, 13 | pool: web::Data, 14 | body: String, 15 | body2: String, 16 | 17 | #[from_request(copy_from_app_data)] 18 | copied_data: u64, 19 | } 20 | 21 | async fn handler(parts: RequestParts) -> impl Responder { 22 | let RequestParts { 23 | method, 24 | pool, 25 | body, 26 | body2, 27 | copied_data, 28 | .. 29 | } = parts; 30 | 31 | let pool = **pool; 32 | 33 | assert_eq!(copied_data, 43); 34 | 35 | assert_eq!(body, "foo"); 36 | 37 | // assert that body is taken and second attempt to do so will be blank 38 | assert_eq!(body2, ""); 39 | 40 | if method == Method::POST && pool == 42 { 41 | HttpResponse::Ok() 42 | } else { 43 | eprintln!("method: {method} | pool: {pool}"); 44 | HttpResponse::NotImplemented() 45 | } 46 | } 47 | 48 | #[actix_web::test] 49 | async fn tdd() { 50 | let srv = actix_test::start(|| { 51 | App::new() 52 | .app_data(43u64) 53 | .app_data(web::Data::new(42u32)) 54 | .default_service(web::to(handler)) 55 | }); 56 | 57 | let res = srv.post("/").send_body("foo").await.unwrap(); 58 | assert_eq!(res.status(), StatusCode::OK); 59 | } 60 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-client-ip-cloudflare" 3 | version = "0.2.0" 4 | authors = ["Rob Ede "] 5 | description = "Extractor for trustworthy client IP addresses when proxied through Cloudflare" 6 | keywords = ["actix", "web", "client", "ip", "cloudflare"] 7 | categories = ["web-programming"] 8 | repository.workspace = true 9 | license.workspace = true 10 | edition.workspace = true 11 | rust-version.workspace = true 12 | 13 | [package.metadata.docs.rs] 14 | rustdoc-args = ["--cfg", "docsrs"] 15 | all-features = true 16 | 17 | [package.metadata.cargo_check_external_types] 18 | allowed_external_types = ["actix_http::*", "actix_utils::*", "actix_web::*", "http::*", "ipnetwork::*"] 19 | 20 | [features] 21 | default = ["fetch-ips"] 22 | fetch-ips = ["fetch-ips-rustls"] 23 | fetch-ips-rustls = ["awc", "awc/rustls-0_23"] 24 | fetch-ips-openssl = ["awc", "awc/openssl"] 25 | 26 | [dependencies] 27 | actix-utils = "3" 28 | actix-web = { version = "4", default-features = false } 29 | awc = { version = "3.5", optional = true } 30 | impl-more = "0.3" 31 | ipnetwork = { version = "0.21", features = ["serde"] } 32 | serde = { version = "1", features = ["derive"] } 33 | tracing = { version = "0.1.41", features = ["log"] } 34 | 35 | [dev-dependencies] 36 | actix-web = "4" 37 | env_logger = "0.11" 38 | 39 | [[example]] 40 | name = "fetch-ips" 41 | required-features = ["fetch-ips"] 42 | 43 | [[example]] 44 | name = "extract-header" 45 | required-features = ["fetch-ips"] 46 | 47 | [lints] 48 | workspace = true 49 | -------------------------------------------------------------------------------- /err-report/README.md: -------------------------------------------------------------------------------- 1 | # err-report 2 | 3 | 4 | 5 | [![crates.io](https://img.shields.io/crates/v/err-report?label=latest)](https://crates.io/crates/err-report) 6 | [![Documentation](https://docs.rs/err-report/badge.svg?version=0.1.2)](https://docs.rs/err-report/0.1.2) 7 | ![Version](https://img.shields.io/badge/rustc-1.75+-ab6000.svg) 8 | ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/err-report.svg) 9 |
10 | [![Dependency Status](https://deps.rs/crate/err-report/0.1.2/status.svg)](https://deps.rs/crate/err-report/0.1.2) 11 | [![Download](https://img.shields.io/crates/d/err-report.svg)](https://crates.io/crates/err-report) 12 | 13 | 14 | 15 | 16 | 17 | Clone of the unstable [`std::error::Report`] type. 18 | 19 | Backtrace support is omitted due to nightly requirement. 20 | 21 | Copied on 2025-09-09. 22 | 23 | ## Examples 24 | 25 | ```rust 26 | use std::ffi::CString; 27 | 28 | use err_report::Report; 29 | 30 | let invalid_utf8 = [b'f', 0xff, b'o', b'o']; 31 | let c_string = CString::new(invalid_utf8).unwrap(); 32 | let err = c_string.into_string().unwrap_err(); 33 | 34 | // without Report, the source/root error is not printed 35 | assert_eq!("C string contained non-utf8 bytes", err.to_string()); 36 | 37 | // with Report, all details in error chain are printed 38 | assert_eq!( 39 | "C string contained non-utf8 bytes: invalid utf-8 sequence of 1 bytes from index 1", 40 | Report::new(err).to_string(), 41 | ); 42 | ``` 43 | 44 | 45 | -------------------------------------------------------------------------------- /russe/benches/results.txt: -------------------------------------------------------------------------------- 1 | decoder fastest │ slowest │ median │ mean │ samples │ iters 2 | ╰─ sse_events 24.92 µs │ 208.5 µs │ 25.38 µs │ 27.41 µs │ 100 │ 100 3 | max alloc: │ │ │ │ │ 4 | 48 │ 48 │ 48 │ 48 │ │ 5 | 21.24 KB │ 21.34 KB │ 21.24 KB │ 21.24 KB │ │ 6 | alloc: │ │ │ │ │ 7 | 108 │ 110 │ 108 │ 108 │ │ 8 | 83.01 KB │ 83.11 KB │ 83.01 KB │ 83.01 KB │ │ 9 | dealloc: │ │ │ │ │ 10 | 108 │ 108 │ 108 │ 108 │ │ 11 | 92.45 KB │ 92.45 KB │ 92.45 KB │ 92.45 KB │ │ 12 | grow: │ │ │ │ │ 13 | 27 │ 27 │ 27 │ 27 │ │ 14 | 11.71 KB │ 11.71 KB │ 11.71 KB │ 11.71 KB │ │ 15 | shrink: │ │ │ │ │ 16 | 5 │ 5 │ 5 │ 5 │ │ 17 | 2.27 KB │ 2.27 KB │ 2.27 KB │ 2.27 KB │ │ 18 | -------------------------------------------------------------------------------- /actix-web-lab/src/msgpack.rs: -------------------------------------------------------------------------------- 1 | //! MessagePack responder. 2 | 3 | use std::sync::LazyLock; 4 | 5 | use actix_web::{HttpRequest, HttpResponse, Responder}; 6 | use bytes::Bytes; 7 | use derive_more::Display; 8 | use mime::Mime; 9 | use serde::Serialize; 10 | 11 | static MSGPACK_MIME: LazyLock = LazyLock::new(|| "application/msgpack".parse().unwrap()); 12 | 13 | /// [MessagePack] responder. 14 | /// 15 | /// If you require the fields to be named, use [`MessagePackNamed`]. 16 | /// 17 | /// [MessagePack]: https://msgpack.org/ 18 | #[derive(Debug, Display)] 19 | pub struct MessagePack(pub T); 20 | 21 | impl_more::impl_deref_and_mut!( in MessagePack => T); 22 | 23 | impl Responder for MessagePack { 24 | type Body = Bytes; 25 | 26 | fn respond_to(self, _req: &HttpRequest) -> HttpResponse { 27 | let body = Bytes::from(rmp_serde::to_vec(&self.0).unwrap()); 28 | 29 | HttpResponse::Ok() 30 | .content_type(MSGPACK_MIME.clone()) 31 | .message_body(body) 32 | .unwrap() 33 | } 34 | } 35 | 36 | /// MessagePack responder with named fields. 37 | #[derive(Debug, Display)] 38 | pub struct MessagePackNamed(pub T); 39 | 40 | impl_more::impl_deref_and_mut!( in MessagePackNamed => T); 41 | 42 | impl Responder for MessagePackNamed { 43 | type Body = Bytes; 44 | 45 | fn respond_to(self, _req: &HttpRequest) -> HttpResponse { 46 | let body = Bytes::from(rmp_serde::to_vec_named(&self.0).unwrap()); 47 | 48 | HttpResponse::Ok() 49 | .content_type(MSGPACK_MIME.clone()) 50 | .message_body(body) 51 | .unwrap() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/src/header_v4.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, net::IpAddr}; 2 | 3 | use actix_web::{ 4 | HttpMessage, error, 5 | http::header::{self, Header, HeaderName, HeaderValue, TryIntoHeaderValue}, 6 | }; 7 | 8 | /// Cloudflare's `cf-connecting-ip` header name. 9 | #[allow(clippy::declare_interior_mutable_const)] 10 | pub const CF_CONNECTING_IP: HeaderName = HeaderName::from_static("cf-connecting-ip"); 11 | 12 | /// Header containing client's IPv4 address when server is behind Cloudflare. 13 | #[derive(Debug, Clone)] 14 | pub enum CfConnectingIp { 15 | /// Extracted client IPv4 address that has been forwarded by a trustworthy peer. 16 | Trusted(IpAddr), 17 | 18 | /// Extracted client IPv4 address that has no trust guarantee. 19 | Untrusted(IpAddr), 20 | } 21 | 22 | impl CfConnectingIp { 23 | /// Returns client IPv4 address, whether trusted or not. 24 | pub fn ip(&self) -> IpAddr { 25 | match self { 26 | Self::Trusted(ip) => *ip, 27 | Self::Untrusted(ip) => *ip, 28 | } 29 | } 30 | } 31 | 32 | impl_more::impl_display_enum! { 33 | CfConnectingIp: 34 | Trusted(ip) => "{ip}", 35 | Untrusted(ip) => "{ip}", 36 | } 37 | 38 | impl TryIntoHeaderValue for CfConnectingIp { 39 | type Error = Infallible; 40 | 41 | fn try_into_value(self) -> Result { 42 | Ok(self.ip().to_string().parse().unwrap()) 43 | } 44 | } 45 | 46 | impl Header for CfConnectingIp { 47 | fn name() -> HeaderName { 48 | CF_CONNECTING_IP 49 | } 50 | 51 | fn parse(msg: &M) -> Result { 52 | let ip = header::from_one_raw_str(msg.headers().get(Self::name()))?; 53 | Ok(Self::Untrusted(ip)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1754487366, 9 | "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1756346337, 24 | "narHash": "sha256-al0UcN5mXrO/p5lcH0MuQaj+t97s3brzCii8GfCBMuA=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "84c26d62ce9e15489c63b83fc44e6eb62705d2c9", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-25.05", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-lib": { 38 | "locked": { 39 | "lastModified": 1753579242, 40 | "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", 41 | "owner": "nix-community", 42 | "repo": "nixpkgs.lib", 43 | "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "nix-community", 48 | "repo": "nixpkgs.lib", 49 | "type": "github" 50 | } 51 | }, 52 | "root": { 53 | "inputs": { 54 | "flake-parts": "flake-parts", 55 | "nixpkgs": "nixpkgs" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /russe/benches/decoder.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use std::{hint::black_box, io, pin::pin}; 4 | 5 | use bytes::Bytes; 6 | use divan::{AllocProfiler, Bencher}; 7 | use futures_test::stream::StreamTestExt as _; 8 | use futures_util::{StreamExt as _, stream}; 9 | use tokio_util::{codec::FramedRead, io::StreamReader}; 10 | 11 | #[global_allocator] 12 | static ALLOC: AllocProfiler = AllocProfiler::system(); 13 | 14 | #[divan::bench] 15 | fn sse_events(b: Bencher<'_, '_>) { 16 | let rt = tokio::runtime::Handle::current(); 17 | 18 | let input = indoc::indoc! {" 19 | retry: 444 20 | 21 | : begin by specifying retry duration 22 | 23 | data: msg1 simple 24 | 25 | data: msg2 26 | data: with more on a newline 27 | 28 | data:msg3 without optional leading space 29 | 30 | data: msg4 with an ID 31 | id: 42 32 | 33 | retry: 999 34 | data: msg5 specifies new retry 35 | id: 43a 36 | 37 | event: msg 38 | data: msg6 is named 39 | 40 | "}; 41 | 42 | b.bench(|| { 43 | let body_stream = stream::iter(black_box(input).as_bytes().chunks(7)) 44 | .map(|line| Ok::<_, io::Error>(Bytes::from(line))) 45 | .interleave_pending(); 46 | let body_reader = StreamReader::new(body_stream); 47 | 48 | let event_stream = FramedRead::new(body_reader, russe::Decoder::default()); 49 | let event_stream = pin!(event_stream); 50 | 51 | let count = rt.block_on(black_box(event_stream).count()); 52 | assert_eq!(count, 8); 53 | }); 54 | } 55 | 56 | fn main() { 57 | let rt = tokio::runtime::Builder::new_current_thread() 58 | .enable_all() 59 | .build() 60 | .unwrap(); 61 | let _guard = rt.enter(); 62 | 63 | divan::main(); 64 | } 65 | -------------------------------------------------------------------------------- /actix-web-lab/examples/body_channel.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates returning a response stream composed of byte chunks sent through a channel-like 2 | //! interface. You can observe the effects using this cURL command: 3 | //! 4 | //! ```sh 5 | //! curl --no-buffer localhost:8080/ 6 | //! ``` 7 | 8 | use std::io; 9 | 10 | use actix_web::{App, HttpResponse, HttpServer, Responder, get, http::header::ContentType, web}; 11 | use actix_web_lab::body; 12 | use tracing::info; 13 | 14 | #[get("/")] 15 | async fn index() -> impl Responder { 16 | let (mut body_tx, body) = body::channel::(); 17 | 18 | // do not wait for this task to finish before sending response 19 | #[allow(clippy::let_underscore_future)] 20 | let _ = web::block(move || { 21 | body_tx.send(web::Bytes::from_static(b"body "))?; 22 | body_tx.send(web::Bytes::from_static(b"from "))?; 23 | 24 | // this is only acceptable due to being inside the `web::block` closure 25 | std::thread::sleep(std::time::Duration::from_millis(1000)); 26 | 27 | body_tx.send(web::Bytes::from_static(b"another "))?; 28 | body_tx.send(web::Bytes::from_static(b"thread"))?; 29 | 30 | // options for closing the stream early: 31 | // body_tx.close(None) 32 | // body_tx.close(Some(io::Error::new(io::ErrorKind::Other, "it broke"))) 33 | 34 | Ok::<_, web::Bytes>(()) 35 | }); 36 | 37 | HttpResponse::Ok() 38 | .insert_header(ContentType::plaintext()) 39 | .message_body(body) 40 | .unwrap() 41 | } 42 | 43 | #[actix_web::main] 44 | async fn main() -> io::Result<()> { 45 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 46 | 47 | info!("staring server at http://localhost:8080"); 48 | 49 | HttpServer::new(|| App::new().service(index)) 50 | .workers(2) 51 | .bind(("127.0.0.1", 8080))? 52 | .run() 53 | .await 54 | } 55 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/src/header_v6.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, net::IpAddr}; 2 | 3 | use actix_web::{ 4 | HttpMessage, error, 5 | http::header::{self, Header, HeaderName, HeaderValue, TryIntoHeaderValue}, 6 | }; 7 | 8 | /// Cloudflare's `cf-connecting-ipv6` header name. 9 | #[allow(clippy::declare_interior_mutable_const)] 10 | pub const CF_CONNECTING_IPV6: HeaderName = HeaderName::from_static("cf-connecting-ipv6"); 11 | 12 | /// Header containing client's IPv6 address when server is behind Cloudflare. 13 | #[derive(Debug, Clone)] 14 | pub enum CfConnectingIpv6 { 15 | /// Extracted client IPv6 address that has been forwarded by a trustworthy peer. 16 | Trusted(IpAddr), 17 | 18 | /// Extracted client IPv6 address that has no trust guarantee. 19 | Untrusted(IpAddr), 20 | } 21 | 22 | impl CfConnectingIpv6 { 23 | /// Returns client IPv6 address, whether trusted or not. 24 | pub fn ip(&self) -> IpAddr { 25 | match self { 26 | Self::Trusted(ip) => *ip, 27 | Self::Untrusted(ip) => *ip, 28 | } 29 | } 30 | 31 | /// Returns `true` if this header is `Trusted`. 32 | #[must_use] 33 | pub fn is_trusted(&self) -> bool { 34 | matches!(self, Self::Trusted(..)) 35 | } 36 | } 37 | 38 | impl_more::impl_display_enum! { 39 | CfConnectingIpv6: 40 | Trusted(ip) => "{ip}", 41 | Untrusted(ip) => "{ip}", 42 | } 43 | 44 | impl TryIntoHeaderValue for CfConnectingIpv6 { 45 | type Error = Infallible; 46 | 47 | fn try_into_value(self) -> Result { 48 | Ok(self.ip().to_string().parse().unwrap()) 49 | } 50 | } 51 | 52 | impl Header for CfConnectingIpv6 { 53 | fn name() -> HeaderName { 54 | CF_CONNECTING_IPV6 55 | } 56 | 57 | fn parse(msg: &M) -> Result { 58 | let ip = header::from_one_raw_str(msg.headers().get(Self::name()))?; 59 | Ok(Self::Untrusted(ip)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /actix-web-lab/examples/map_response.rs: -------------------------------------------------------------------------------- 1 | //! Shows a couple of ways to use the `from_fn` middleware. 2 | 3 | use std::io; 4 | 5 | use actix_web::{ 6 | App, Error, HttpRequest, HttpResponse, HttpServer, body::MessageBody, dev::ServiceResponse, 7 | http::header, middleware::Logger, web, 8 | }; 9 | use actix_web_lab::middleware::{ConditionOption, map_response, map_response_body}; 10 | use tracing::info; 11 | 12 | async fn add_res_header( 13 | mut res: ServiceResponse, 14 | ) -> Result, Error> { 15 | res.headers_mut() 16 | .insert(header::WARNING, header::HeaderValue::from_static("42")); 17 | 18 | Ok(res) 19 | } 20 | 21 | async fn mutate_body_type( 22 | _req: HttpRequest, 23 | _body: impl MessageBody + 'static, 24 | ) -> Result { 25 | Ok("foo".to_owned()) 26 | } 27 | 28 | #[actix_web::main] 29 | async fn main() -> io::Result<()> { 30 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 31 | 32 | let bind = ("127.0.0.1", 8080); 33 | info!("staring server at http://{}:{}", &bind.0, &bind.1); 34 | 35 | HttpServer::new(|| { 36 | let mutator_enabled = false; 37 | let mutate_body_type_mw = 38 | ConditionOption::from(mutator_enabled.then(|| map_response_body(mutate_body_type))); 39 | 40 | App::new() 41 | .service( 42 | web::resource("/foo") 43 | .default_service(web::to(HttpResponse::Ok)) 44 | .wrap(Logger::default()) 45 | .wrap(map_response(add_res_header)), 46 | ) 47 | .service( 48 | web::resource("/bar") 49 | .default_service(web::to(HttpResponse::Ok)) 50 | .wrap(mutate_body_type_mw) 51 | .wrap(Logger::default()), 52 | ) 53 | .default_service(web::to(HttpResponse::Ok)) 54 | }) 55 | .workers(1) 56 | .bind(bind)? 57 | .run() 58 | .await 59 | } 60 | -------------------------------------------------------------------------------- /actix-hash/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-hash" 3 | version = "0.5.0" 4 | authors = ["Rob Ede "] 5 | description = "Hashing utilities for Actix Web" 6 | keywords = ["actix", "http", "web", "request", "hash"] 7 | categories = ["web-programming::http-server", "cryptography"] 8 | repository.workspace = true 9 | license.workspace = true 10 | edition.workspace = true 11 | rust-version.workspace = true 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | rustdoc-args = ["--cfg", "docsrs"] 16 | 17 | [package.metadata.cargo_check_external_types] 18 | allowed_external_types = [ 19 | "actix_http::*", 20 | "actix_utils::*", 21 | "actix_web::*", 22 | "blake2::*", 23 | "blake3::*", 24 | "crypto_common::*", 25 | "digest::*", 26 | "generic_array::*", 27 | "md4::*", 28 | "md5::*", 29 | "sha1::*", 30 | "sha2::*", 31 | "sha3::*", 32 | ] 33 | 34 | [features] 35 | default = ["blake2", "blake3", "md4", "md5", "sha1", "sha2", "sha3"] 36 | blake2 = ["dep:blake2"] 37 | blake3 = ["dep:blake3"] 38 | md4 = ["dep:md4"] 39 | md5 = ["dep:md5"] 40 | sha1 = ["dep:sha1"] 41 | sha2 = ["dep:sha2"] 42 | sha3 = ["dep:sha3"] 43 | 44 | [dependencies] 45 | actix-http = "3" 46 | actix-web = { version = "4", default-features = false } 47 | actix-web-lab = "0.24" 48 | futures-core = "0.3.17" 49 | futures-util = { version = "0.3.31", default-features = false, features = ["std"] } 50 | pin-project-lite = "0.2" 51 | tracing = { version = "0.1.41", features = ["log"] } 52 | 53 | blake2 = { package = "blake2", version = "0.10", optional = true } 54 | blake3 = { package = "blake3", version = "1.6", optional = true, features = ["traits-preview"] } 55 | digest = "0.10" 56 | md4 = { package = "md4", version = "0.10", optional = true } 57 | md5 = { package = "md-5", version = "0.10", optional = true } 58 | sha1 = { package = "sha1", version = "0.10", optional = true } 59 | sha2 = { package = "sha2", version = "0.10", optional = true } 60 | sha3 = { package = "sha3", version = "0.10", optional = true } 61 | subtle = "2" 62 | 63 | [dev-dependencies] 64 | actix-web = "4" 65 | env_logger = "0.11" 66 | hex-literal = "0.4" 67 | sha2 = "0.10" 68 | 69 | [[test]] 70 | name = "body_hash" 71 | required-features = ["sha2"] 72 | 73 | [[example]] 74 | name = "body_sha2" 75 | required-features = ["sha2"] 76 | 77 | [lints] 78 | workspace = true 79 | -------------------------------------------------------------------------------- /actix-web-lab/src/host.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use actix_utils::future::{Ready, ok}; 4 | use actix_web::{FromRequest, HttpRequest, dev::Payload}; 5 | 6 | /// Host information. 7 | /// 8 | /// See [`ConnectionInfo::host()`](actix_web::dev::ConnectionInfo::host) for more. 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | pub struct Host(String); 11 | 12 | impl_more::impl_as_ref!(Host => String); 13 | impl_more::impl_into!(Host => String); 14 | impl_more::forward_display!(Host); 15 | 16 | impl Host { 17 | /// Unwraps into inner string value. 18 | pub fn into_inner(self) -> String { 19 | self.0 20 | } 21 | } 22 | 23 | impl FromRequest for Host { 24 | type Error = Infallible; 25 | type Future = Ready>; 26 | 27 | #[inline] 28 | fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { 29 | ok(Host(req.connection_info().host().to_owned())) 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use actix_web::{ 36 | App, HttpResponse, 37 | http::StatusCode, 38 | test::{self, TestRequest}, 39 | web, 40 | }; 41 | 42 | use super::*; 43 | 44 | #[actix_web::test] 45 | async fn extracts_host() { 46 | let app = 47 | test::init_service(App::new().default_service(web::to(|host: Host| async move { 48 | HttpResponse::Ok().body(host.to_string()) 49 | }))) 50 | .await; 51 | 52 | let req = TestRequest::default() 53 | .insert_header(("host", "in-header.com")) 54 | .to_request(); 55 | let res = test::call_service(&app, req).await; 56 | assert_eq!(res.status(), StatusCode::OK); 57 | assert_eq!(test::read_body(res).await, b"in-header.com".as_ref()); 58 | 59 | let req = TestRequest::default().uri("http://in-url.com").to_request(); 60 | let res = test::call_service(&app, req).await; 61 | assert_eq!(res.status(), StatusCode::OK); 62 | assert_eq!(test::read_body(res).await, b"in-url.com".as_ref()); 63 | 64 | let req = TestRequest::default().to_request(); 65 | let res = test::call_service(&app, req).await; 66 | assert_eq!(res.status(), StatusCode::OK); 67 | assert_eq!(test::read_body(res).await, b"localhost:8080".as_ref()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /actix-web-lab/src/body_async_write.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | pin::Pin, 4 | task::{Context, Poll, ready}, 5 | }; 6 | 7 | use actix_web::body::{BodySize, MessageBody}; 8 | use bytes::Bytes; 9 | use tokio::{ 10 | io::AsyncWrite, 11 | sync::mpsc::{UnboundedReceiver, UnboundedSender}, 12 | }; 13 | 14 | /// Returns an `AsyncWrite` response body writer and its associated body type. 15 | /// 16 | /// # Examples 17 | /// ``` 18 | /// # use actix_web::{HttpResponse, web}; 19 | /// use actix_web_lab::body; 20 | /// use tokio::io::AsyncWriteExt as _; 21 | /// 22 | /// # async fn index() { 23 | /// let (mut wrt, body) = body::writer(); 24 | /// 25 | /// let _ = tokio::spawn(async move { wrt.write_all(b"body from another thread").await }); 26 | /// 27 | /// HttpResponse::Ok().body(body) 28 | /// # ;} 29 | /// ``` 30 | pub fn writer() -> (Writer, impl MessageBody) { 31 | let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); 32 | (Writer { tx }, BodyStream { rx }) 33 | } 34 | 35 | /// An `AsyncWrite` response body writer. 36 | #[derive(Debug, Clone)] 37 | pub struct Writer { 38 | tx: UnboundedSender, 39 | } 40 | 41 | impl AsyncWrite for Writer { 42 | fn poll_write( 43 | self: Pin<&mut Self>, 44 | _cx: &mut Context<'_>, 45 | buf: &[u8], 46 | ) -> Poll> { 47 | self.tx 48 | .send(Bytes::copy_from_slice(buf)) 49 | .map_err(io::Error::other)?; 50 | 51 | Poll::Ready(Ok(buf.len())) 52 | } 53 | 54 | fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 55 | Poll::Ready(Ok(())) 56 | } 57 | 58 | fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 59 | Poll::Ready(Ok(())) 60 | } 61 | } 62 | 63 | #[derive(Debug)] 64 | struct BodyStream { 65 | rx: UnboundedReceiver, 66 | } 67 | 68 | impl MessageBody for BodyStream { 69 | type Error = io::Error; 70 | 71 | fn size(&self) -> BodySize { 72 | BodySize::Stream 73 | } 74 | 75 | fn poll_next( 76 | mut self: Pin<&mut Self>, 77 | cx: &mut Context<'_>, 78 | ) -> Poll>> { 79 | Poll::Ready(ready!(self.rx.poll_recv(cx)).map(Ok)) 80 | } 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | use super::*; 86 | 87 | static_assertions::assert_impl_all!(Writer: Send, Sync, Unpin); 88 | static_assertions::assert_impl_all!(BodyStream: Send, Sync, Unpin, MessageBody); 89 | } 90 | -------------------------------------------------------------------------------- /actix-web-lab/examples/fork_request_payload.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates forking a request payload so that multiple extractors can derive data from a body. 2 | //! 3 | //! ```sh 4 | //! curl -X POST localhost:8080/ -d 'foo' 5 | //! 6 | //! # or using HTTPie 7 | //! http POST :8080/ --raw foo 8 | //! ``` 9 | 10 | use std::io; 11 | 12 | use actix_web::{App, FromRequest, HttpRequest, HttpServer, dev, middleware, web}; 13 | use actix_web_lab::util::fork_request_payload; 14 | use futures_util::{TryFutureExt as _, future::LocalBoxFuture}; 15 | use tokio::try_join; 16 | use tracing::info; 17 | 18 | struct TwoBodies(T, U); 19 | 20 | impl TwoBodies { 21 | fn into_parts(self) -> (T, U) { 22 | (self.0, self.1) 23 | } 24 | } 25 | 26 | impl FromRequest for TwoBodies 27 | where 28 | T: FromRequest, 29 | T::Future: 'static, 30 | U: FromRequest, 31 | U::Future: 'static, 32 | { 33 | type Error = actix_web::Error; 34 | type Future = LocalBoxFuture<'static, Result>; 35 | 36 | fn from_request(req: &HttpRequest, pl: &mut dev::Payload) -> Self::Future { 37 | let mut forked_pl = fork_request_payload(pl); 38 | 39 | let t_fut = T::from_request(req, pl); 40 | let u_fut = U::from_request(req, &mut forked_pl); 41 | 42 | Box::pin(async move { 43 | // .err_into to align error types to actix_web::Error 44 | let (t, u) = try_join!(t_fut.err_into(), u_fut.err_into())?; 45 | Ok(Self(t, u)) 46 | }) 47 | } 48 | } 49 | 50 | #[actix_web::main] 51 | async fn main() -> io::Result<()> { 52 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 53 | 54 | info!("staring server at http://localhost:8080"); 55 | 56 | HttpServer::new(|| { 57 | App::new() 58 | .wrap(middleware::Logger::default().log_target("@")) 59 | .route( 60 | "/", 61 | web::post().to(|body: TwoBodies| async move { 62 | let (string, bytes) = body.into_parts(); 63 | 64 | // proves that body was extracted twice since the bytes extracted are byte-equal to 65 | // the string, without forking the request payload, the bytes parts would be empty 66 | assert_eq!(string.as_bytes(), &bytes); 67 | 68 | // echo string 69 | string 70 | }), 71 | ) 72 | }) 73 | .workers(1) 74 | .bind(("127.0.0.1", 8080))? 75 | .run() 76 | .await 77 | } 78 | -------------------------------------------------------------------------------- /actix-web-lab/src/test_request_macros.rs: -------------------------------------------------------------------------------- 1 | /// Create a `TestRequest` using a DSL that looks kinda like on-the-wire HTTP/1.x requests. 2 | /// 3 | /// # Examples 4 | /// ``` 5 | /// use actix_web::test::TestRequest; 6 | /// use actix_web_lab::test_request; 7 | /// 8 | /// let _req: TestRequest = test_request! { 9 | /// POST "/"; 10 | /// "Origin" => "example.com" 11 | /// "Access-Control-Request-Method" => "POST" 12 | /// "Access-Control-Request-Headers" => "Content-Type, X-CSRF-TOKEN"; 13 | /// @json {"abc": "123"} 14 | /// }; 15 | /// 16 | /// let _req: TestRequest = test_request! { 17 | /// POST "/"; 18 | /// "Content-Type" => "application/json" 19 | /// "Origin" => "example.com" 20 | /// "Access-Control-Request-Method" => "POST" 21 | /// "Access-Control-Request-Headers" => "Content-Type, X-CSRF-TOKEN"; 22 | /// @raw r#"{"abc": "123"}"# 23 | /// }; 24 | /// ``` 25 | #[macro_export] 26 | macro_rules! test_request { 27 | ($method:ident $uri:expr) => {{ 28 | ::actix_web::test::TestRequest::default() 29 | .method(::actix_web::http::Method::$method) 30 | .uri($uri) 31 | }}; 32 | 33 | ($method:ident $uri:expr; $($hdr_name:expr => $hdr_val:expr)+) => {{ 34 | test_request!($method $uri) 35 | $( 36 | .insert_header(($hdr_name, $hdr_val)) 37 | )+ 38 | }}; 39 | 40 | ($method:ident $uri:expr; $($hdr_name:expr => $hdr_val:expr)+; @json $payload:tt) => {{ 41 | test_request!($method $uri; $($hdr_name => $hdr_val)+) 42 | .set_json($crate::__reexports::serde_json::json!($payload)) 43 | }}; 44 | 45 | ($method:ident $uri:expr; $($hdr_name:expr => $hdr_val:expr)+; @raw $payload:expr) => {{ 46 | test_request!($method $uri; $($hdr_name => $hdr_val)+) 47 | .set_payload($payload) 48 | }}; 49 | } 50 | 51 | pub use test_request; 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use actix_web::test::TestRequest; 56 | 57 | use super::*; 58 | 59 | #[test] 60 | fn request_builder() { 61 | let _req: TestRequest = test_request! { 62 | POST "/"; 63 | "Origin" => "example.com" 64 | "Access-Control-Request-Method" => "POST" 65 | "Access-Control-Request-Headers" => "Content-Type, X-CSRF-TOKEN"; 66 | @json { "abc": "123" } 67 | }; 68 | 69 | let _req: TestRequest = test_request! { 70 | POST "/"; 71 | "Content-Type" => "application/json" 72 | "Origin" => "example.com" 73 | "Access-Control-Request-Method" => "POST" 74 | "Access-Control-Request-Headers" => "Content-Type, X-CSRF-TOKEN"; 75 | @raw r#"{"abc": "123"}"# 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /actix-web-lab/examples/body_hmac.rs: -------------------------------------------------------------------------------- 1 | //! Alternative approach to using `BodyHmac` type using more flexible `RequestSignature` type. 2 | 3 | use std::io; 4 | 5 | use actix_web::{ 6 | App, Error, FromRequest, HttpRequest, HttpServer, 7 | middleware::Logger, 8 | web::{self, Bytes, Data}, 9 | }; 10 | use actix_web_lab::extract::{RequestSignature, RequestSignatureScheme}; 11 | use digest::{CtOutput, Mac}; 12 | use hmac::SimpleHmac; 13 | use sha2::Sha256; 14 | use tracing::info; 15 | 16 | struct AbcSigningKey([u8; 32]); 17 | 18 | /// Grabs variable signing key from app data. 19 | async fn get_signing_key(req: &HttpRequest) -> actix_web::Result<[u8; 32]> { 20 | let key = Data::::extract(req).into_inner()?.0; 21 | Ok(key) 22 | } 23 | 24 | #[derive(Debug)] 25 | struct AbcApi { 26 | /// Payload hash state. 27 | hmac: SimpleHmac, 28 | } 29 | 30 | impl RequestSignatureScheme for AbcApi { 31 | type Signature = CtOutput>; 32 | type Error = Error; 33 | 34 | async fn init(req: &HttpRequest) -> Result { 35 | let key = get_signing_key(req).await?; 36 | 37 | Ok(Self { 38 | hmac: SimpleHmac::new_from_slice(&key).unwrap(), 39 | }) 40 | } 41 | 42 | async fn consume_chunk(&mut self, _req: &HttpRequest, chunk: Bytes) -> Result<(), Self::Error> { 43 | self.hmac.update(&chunk); 44 | Ok(()) 45 | } 46 | 47 | async fn finalize(self, _req: &HttpRequest) -> Result { 48 | Ok(self.hmac.finalize()) 49 | } 50 | 51 | fn verify( 52 | signature: Self::Signature, 53 | _req: &HttpRequest, 54 | ) -> Result { 55 | // pass-through signature since verification is not required for this scheme 56 | // (shown for completeness, this is the default impl of `verify` and could be removed) 57 | Ok(signature) 58 | } 59 | } 60 | 61 | #[actix_web::main] 62 | async fn main() -> io::Result<()> { 63 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 64 | 65 | info!("staring server at http://localhost:8080"); 66 | 67 | HttpServer::new(|| { 68 | App::new() 69 | .wrap(Logger::default().log_target("@")) 70 | .app_data(Data::new(AbcSigningKey([0; 32]))) 71 | .route( 72 | "/", 73 | web::post().to(|body: RequestSignature| async move { 74 | let (body, sig) = body.into_parts(); 75 | let sig = sig.into_bytes().to_vec(); 76 | format!("{body:?}\n\n{sig:x?}") 77 | }), 78 | ) 79 | }) 80 | .workers(1) 81 | .bind(("127.0.0.1", 8080))? 82 | .run() 83 | .await 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | merge_group: 7 | types: [checks_requested] 8 | push: 9 | branches: [main] 10 | 11 | permissions: 12 | contents: read 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | read_msrv: 20 | name: Read MSRV 21 | uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0 22 | 23 | build_and_test: 24 | needs: read_msrv 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | version: 30 | - { name: msrv, version: "${{ needs.read_msrv.outputs.msrv }}" } 31 | - { name: stable, version: stable } 32 | 33 | name: Test / ${{ matrix.version.name }} 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@v5 38 | 39 | - name: Setup mold linker 40 | uses: rui314/setup-mold@v1 41 | 42 | - name: Install Rust (${{ matrix.version.name }}) 43 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 44 | with: 45 | toolchain: ${{ matrix.version.version }} 46 | 47 | - name: Install just,cargo-hack,cargo-nextest,cargo-ci-cache-clean 48 | uses: taiki-e/install-action@v2.62.44 49 | with: 50 | tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean 51 | 52 | - name: workaround MSRV issues 53 | if: matrix.version.name == 'msrv' 54 | run: just downgrade-for-msrv 55 | 56 | - name: tests 57 | timeout-minutes: 60 58 | run: just test 59 | 60 | - name: CI cache clean 61 | run: cargo-ci-cache-clean 62 | 63 | rustdoc: 64 | name: Documentation Tests 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v5 68 | 69 | - name: Install Rust (nightly) 70 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 71 | with: 72 | toolchain: nightly 73 | 74 | - name: Install just 75 | uses: taiki-e/install-action@v2.62.44 76 | with: 77 | tool: just 78 | 79 | - name: doc tests 80 | run: just test-docs 81 | 82 | features: 83 | name: Feature Combinations 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v5 87 | 88 | - name: Install Rust (stable) 89 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 90 | 91 | - name: Install cargo-hack 92 | uses: taiki-e/install-action@v2.62.44 93 | with: 94 | tool: cargo-hack 95 | 96 | - name: doc tests 97 | run: | 98 | cargo hack --each-feature check 99 | cargo hack --each-feature check --all-targets 100 | -------------------------------------------------------------------------------- /actix-web-lab/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! In-progress extractors and middleware for Actix Web. 2 | //! 3 | //! # What Is This Crate? 4 | //! This crate serves as a preview and test ground for upcoming features and ideas for Actix Web's 5 | //! built in library of extractors, middleware and other utilities. 6 | //! 7 | //! Any kind of feedback is welcome. 8 | //! 9 | //! # Complete Examples 10 | //! See [the `examples` folder][examples] for some complete examples of items in this crate. 11 | //! 12 | //! # Things To Know About This Crate 13 | //! - It will never reach v1.0. 14 | //! - Minimum Supported Rust Version (MSRV) is latest stable at the time of each release. 15 | //! - Breaking changes will likely happen on most 0.x version bumps. 16 | //! - Documentation might be limited for some items. 17 | //! - Items that graduate to Actix Web crate will be marked deprecated here for a reasonable amount 18 | //! of time so you can migrate. 19 | //! - Migrating will often be as easy as dropping the `_lab` suffix from imports when migrating. 20 | //! 21 | //! [examples]: https://github.com/robjtede/actix-web-lab/tree/HEAD/actix-web-lab/examples 22 | 23 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 24 | 25 | mod body_async_write; 26 | mod body_channel; 27 | mod body_limit; 28 | mod bytes; 29 | mod cache_control; 30 | mod catch_panic; 31 | #[cfg(feature = "cbor")] 32 | mod cbor; 33 | mod clear_site_data; 34 | mod condition_option; 35 | mod content_length; 36 | mod csv; 37 | mod display_stream; 38 | mod err_handler; 39 | mod forwarded; 40 | mod host; 41 | mod infallible_body_stream; 42 | mod json; 43 | mod lazy_data; 44 | mod lazy_data_shared; 45 | mod load_shed; 46 | mod local_data; 47 | mod middleware_map_response; 48 | mod middleware_map_response_body; 49 | #[cfg(feature = "msgpack")] 50 | mod msgpack; 51 | mod ndjson; 52 | mod normalize_path; 53 | mod panic_reporter; 54 | mod path; 55 | mod query; 56 | mod redirect_to_https; 57 | mod redirect_to_non_www; 58 | mod redirect_to_www; 59 | mod request_signature; 60 | #[cfg(feature = "spa")] 61 | mod spa; 62 | mod strict_transport_security; 63 | mod swap_data; 64 | #[cfg(test)] 65 | mod test_header_macros; 66 | mod test_request_macros; 67 | mod test_response_macros; 68 | mod test_services; 69 | mod url_encoded_form; 70 | mod x_forwarded_prefix; 71 | 72 | // public API 73 | pub mod body; 74 | pub mod extract; 75 | pub mod guard; 76 | pub mod header; 77 | pub mod middleware; 78 | pub mod respond; 79 | pub mod sse; 80 | pub mod test; 81 | pub mod util; 82 | pub mod web; 83 | 84 | #[cfg(feature = "derive")] 85 | pub use actix_web_lab_derive::FromRequest; 86 | 87 | // private re-exports for macros 88 | #[doc(hidden)] 89 | pub mod __reexports { 90 | pub use ::actix_web; 91 | pub use ::futures_util; 92 | pub use ::serde_json; 93 | pub use ::tokio; 94 | pub use ::tracing; 95 | } 96 | 97 | pub(crate) type BoxError = Box; 98 | -------------------------------------------------------------------------------- /actix-web-lab/examples/query.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates use of alternative Query extractor with better deserializer and errors. 2 | 3 | use std::io; 4 | 5 | use actix_web::{ 6 | App, HttpResponse, HttpServer, Resource, Responder, 7 | error::ErrorBadRequest, 8 | middleware::{Logger, NormalizePath}, 9 | }; 10 | use actix_web_lab::extract::{Query, QueryDeserializeError}; 11 | use serde::{Deserialize, Serialize}; 12 | 13 | #[derive(Debug, Deserialize, Serialize)] 14 | #[serde(rename_all = "kebab-case")] 15 | enum Type { 16 | Planet, 17 | Moon, 18 | } 19 | 20 | #[derive(Debug, Deserialize, Serialize)] 21 | struct Params { 22 | /// Limit number of results. 23 | count: u32, 24 | 25 | /// Filter by object type. 26 | #[serde(rename = "type", default)] 27 | types: Vec, 28 | } 29 | 30 | /// Demonstrates multiple query parameters and getting path from deserialization errors. 31 | async fn query( 32 | query: Result, QueryDeserializeError>, 33 | ) -> actix_web::Result { 34 | let params = match query { 35 | Ok(Query(query)) => query, 36 | Err(err) => return Err(ErrorBadRequest(err)), 37 | }; 38 | 39 | tracing::debug!("filters: {params:?}"); 40 | 41 | Ok(HttpResponse::Ok().json(params)) 42 | } 43 | 44 | /// Baseline comparison using the built-in `Query` extractor. 45 | async fn baseline( 46 | query: actix_web::Result>, 47 | ) -> actix_web::Result { 48 | let params = query?.0; 49 | 50 | tracing::debug!("filters: {params:?}"); 51 | 52 | Ok(HttpResponse::Ok().json(params)) 53 | } 54 | 55 | #[actix_web::main] 56 | async fn main() -> io::Result<()> { 57 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 58 | 59 | tracing::info!("starting HTTP server at http://localhost:8080"); 60 | 61 | HttpServer::new(|| { 62 | App::new() 63 | .service(Resource::new("/").get(query)) 64 | .service(Resource::new("/baseline").get(baseline)) 65 | .wrap(NormalizePath::trim()) 66 | .wrap(Logger::default()) 67 | }) 68 | .bind(("127.0.0.1", 8080))? 69 | .workers(2) 70 | .run() 71 | .await 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use actix_web::{App, body::to_bytes, dev::Service, http::StatusCode, test, web}; 77 | 78 | use super::*; 79 | 80 | #[actix_web::test] 81 | async fn test_index() { 82 | let app = 83 | test::init_service(App::new().service(web::resource("/").route(web::post().to(query)))) 84 | .await; 85 | 86 | let req = test::TestRequest::post() 87 | .uri("/?count=5&type=planet&type=moon") 88 | .to_request(); 89 | 90 | let res = app.call(req).await.unwrap(); 91 | assert_eq!(res.status(), StatusCode::OK); 92 | 93 | let body_bytes = to_bytes(res.into_body()).await.unwrap(); 94 | assert_eq!(body_bytes, r#"{"count":5,"type":["planet","moon"]}"#); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /err-report/tests/tests.rs: -------------------------------------------------------------------------------- 1 | #![expect(missing_docs)] 2 | 3 | use std::{error::Error as StdError, fmt, io}; 4 | 5 | use err_report::Report; 6 | 7 | #[derive(Debug)] 8 | struct WrappedError(E); 9 | 10 | impl fmt::Display for WrappedError { 11 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 12 | f.write_str("Wrapped") 13 | } 14 | } 15 | 16 | impl StdError for WrappedError 17 | where 18 | E: StdError + 'static, 19 | { 20 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 21 | Some(&self.0) 22 | } 23 | } 24 | 25 | #[test] 26 | fn standalone_error_single_line() { 27 | assert_eq!( 28 | "Verbatim", 29 | Report::new(io::Error::other("Verbatim")).to_string(), 30 | ); 31 | 32 | assert_eq!( 33 | "Passed non-utf8 string", 34 | Report::new(io::Error::new( 35 | io::ErrorKind::InvalidData, 36 | "Passed non-utf8 string" 37 | )) 38 | .to_string(), 39 | ); 40 | } 41 | 42 | #[test] 43 | fn wrapped_error_single_line() { 44 | assert_eq!( 45 | "Wrapped: Verbatim", 46 | Report::new(WrappedError(io::Error::other("Verbatim"))).to_string(), 47 | ); 48 | 49 | assert_eq!( 50 | "Wrapped: Passed non-utf8 string", 51 | Report::new(WrappedError(io::Error::new( 52 | io::ErrorKind::InvalidData, 53 | "Passed non-utf8 string" 54 | ))) 55 | .to_string(), 56 | ); 57 | } 58 | 59 | #[test] 60 | fn standalone_error_pretty() { 61 | assert_eq!( 62 | "Verbatim", 63 | Report::new(io::Error::other("Verbatim")) 64 | .pretty(true) 65 | .to_string(), 66 | ); 67 | 68 | assert_eq!( 69 | "Passed non-utf8 string", 70 | Report::new(io::Error::new( 71 | io::ErrorKind::InvalidData, 72 | "Passed non-utf8 string" 73 | )) 74 | .pretty(true) 75 | .to_string(), 76 | ); 77 | } 78 | 79 | #[test] 80 | fn wrapped_error_pretty() { 81 | assert_eq!( 82 | indoc::indoc! {" 83 | Wrapped 84 | 85 | Caused by: 86 | Verbatim"}, 87 | Report::new(WrappedError(io::Error::other("Verbatim"))) 88 | .pretty(true) 89 | .to_string(), 90 | ); 91 | 92 | assert_eq!( 93 | indoc::indoc! {" 94 | Wrapped 95 | 96 | Caused by: 97 | Root error"}, 98 | Report::new(WrappedError(io::Error::other("Root error"))) 99 | .pretty(true) 100 | .to_string(), 101 | ); 102 | 103 | assert_eq!( 104 | indoc::indoc! {" 105 | Wrapped 106 | 107 | Caused by: 108 | 0: Wrapped 109 | 1: Root error"}, 110 | Report::new(WrappedError(WrappedError(io::Error::other("Root error")))) 111 | .pretty(true) 112 | .to_string(), 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /actix-hash/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Hashing utilities for Actix Web. 2 | //! 3 | //! # Crate Features 4 | //! All features are enabled by default. 5 | //! - `blake2`: Blake2 types 6 | //! - `blake3`: Blake3 types 7 | //! - `md5`: MD5 types 🚩 8 | //! - `md4`: MD4 types 🚩 9 | //! - `sha1`: SHA-1 types 🚩 10 | //! - `sha2`: SHA-2 types 11 | //! - `sha3`: SHA-3 types 12 | //! 13 | //! # Security Warning 🚩 14 | //! The `md4`, `md5`, and `sha1` types are included for completeness and interoperability but they 15 | //! are considered cryptographically broken by modern standards. For security critical use cases, 16 | //! you should move to using the other algorithms. 17 | 18 | #![forbid(unsafe_code)] 19 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 20 | 21 | mod body_hash; 22 | 23 | pub use self::body_hash::{BodyHash, BodyHashParts}; 24 | 25 | macro_rules! body_hash_alias { 26 | ($name:ident, $digest:path, $feature:literal, $desc:literal, $out_size:literal) => { 27 | #[doc = concat!("Wraps an extractor and calculates a `", $desc, "` body checksum hash alongside.")] 28 | /// # Example 29 | /// 30 | /// ``` 31 | #[doc = concat!("use actix_hash::", stringify!($name), ";")] 32 | /// 33 | #[doc = concat!("async fn handler(body: ", stringify!($name), ") -> String {")] 34 | #[doc = concat!(" assert_eq!(body.hash().len(), ", $out_size, ");")] 35 | /// body.into_parts().inner 36 | /// } 37 | /// # 38 | /// # // test that the documented hash size is correct 39 | #[doc = concat!("# type Hasher = ", stringify!($digest), ";")] 40 | #[doc = concat!("# const OUT_SIZE: usize = ", $out_size, ";")] 41 | /// # assert_eq!( 42 | /// # digest::generic_array::GenericArray::::OutputSize 44 | /// # >::default().len(), 45 | /// # OUT_SIZE, 46 | /// # ); 47 | /// ``` 48 | #[cfg(feature = $feature)] 49 | pub type $name = BodyHash; 50 | }; 51 | } 52 | 53 | // Obsolete 54 | body_hash_alias!(BodyMd4, md4::Md4, "md4", "MD4", 16); 55 | body_hash_alias!(BodyMd5, md5::Md5, "md5", "MD5", 16); 56 | body_hash_alias!(BodySha1, sha1::Sha1, "sha1", "SHA-1", 20); 57 | 58 | // SHA-2 59 | body_hash_alias!(BodySha224, sha2::Sha224, "sha2", "SHA-224", 28); 60 | body_hash_alias!(BodySha256, sha2::Sha256, "sha2", "SHA-256", 32); 61 | body_hash_alias!(BodySha384, sha2::Sha384, "sha2", "SHA-384", 48); 62 | body_hash_alias!(BodySha512, sha2::Sha512, "sha2", "SHA-512", 64); 63 | 64 | // SHA-3 65 | body_hash_alias!(BodySha3_224, sha3::Sha3_224, "sha3", "SHA-3-224", 28); 66 | body_hash_alias!(BodySha3_256, sha3::Sha3_256, "sha3", "SHA-3-256", 32); 67 | body_hash_alias!(BodySha3_384, sha3::Sha3_384, "sha3", "SHA-3-384", 48); 68 | body_hash_alias!(BodySha3_512, sha3::Sha3_512, "sha3", "SHA-3-512", 64); 69 | 70 | // Blake2 71 | body_hash_alias!(BodyBlake2b, blake2::Blake2b512, "blake2", "Blake2b", 64); 72 | body_hash_alias!(BodyBlake2s, blake2::Blake2s256, "blake2", "Blake2s", 32); 73 | 74 | // Blake3 75 | body_hash_alias!(BodyBlake3, blake3::Hasher, "blake3", "Blake3", 32); 76 | -------------------------------------------------------------------------------- /actix-web-lab/src/test_header_macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! header_test_module { 2 | ($id:ident, $tm:ident{$($tf:item)*}) => { 3 | #[cfg(test)] 4 | mod $tm { 5 | #![allow(unused_imports)] 6 | 7 | use ::core::str; 8 | 9 | use ::actix_http::{Method, test}; 10 | use ::actix_web::http::header; 11 | use ::mime::*; 12 | 13 | use crate::test::*; 14 | use super::{$id as HeaderField, *}; 15 | 16 | $($tf)* 17 | } 18 | } 19 | } 20 | 21 | macro_rules! header_round_trip_test { 22 | ($id:ident, $raw:expr) => { 23 | #[test] 24 | fn $id() { 25 | use ::actix_http::test; 26 | 27 | let raw = $raw; 28 | let headers = raw.iter().map(|x| x.to_vec()).collect::>(); 29 | 30 | let mut req = test::TestRequest::default(); 31 | 32 | for item in headers { 33 | req = req.append_header((HeaderField::name(), item)).take(); 34 | } 35 | 36 | let req = req.finish(); 37 | let value = HeaderField::parse(&req); 38 | 39 | let result = format!("{}", value.unwrap()); 40 | let expected = ::std::string::String::from_utf8(raw[0].to_vec()).unwrap(); 41 | 42 | let result_cmp: Vec = result 43 | .to_ascii_lowercase() 44 | .split(' ') 45 | .map(|x| x.to_owned()) 46 | .collect(); 47 | let expected_cmp: Vec = expected 48 | .to_ascii_lowercase() 49 | .split(' ') 50 | .map(|x| x.to_owned()) 51 | .collect(); 52 | 53 | assert_eq!(result_cmp.concat(), expected_cmp.concat()); 54 | } 55 | }; 56 | 57 | ($id:ident, $raw:expr, $exp:expr) => { 58 | #[test] 59 | fn $id() { 60 | use actix_http::test; 61 | 62 | let headers = $raw.iter().map(|x| x.to_vec()).collect::>(); 63 | let mut req = test::TestRequest::default(); 64 | 65 | for item in headers { 66 | req.append_header((HeaderField::name(), item)); 67 | } 68 | 69 | let req = req.finish(); 70 | let val = HeaderField::parse(&req); 71 | 72 | let exp: ::core::option::Option = $exp; 73 | 74 | // test parsing 75 | assert_eq!(val.ok(), exp); 76 | 77 | // test formatting 78 | if let Some(exp) = exp { 79 | let raw = &($raw)[..]; 80 | let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap()); 81 | let mut joined = String::new(); 82 | if let Some(s) = iter.next() { 83 | joined.push_str(s); 84 | for s in iter { 85 | joined.push_str(", "); 86 | joined.push_str(s); 87 | } 88 | } 89 | assert_eq!(format!("{}", exp), joined); 90 | } 91 | } 92 | }; 93 | } 94 | 95 | pub(crate) use header_round_trip_test; 96 | pub(crate) use header_test_module; 97 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Extractor for trustworthy client IP addresses when proxied through Cloudflare. 2 | //! 3 | //! When traffic to your web server is proxied through Cloudflare, it is tagged with headers 4 | //! containing the original client's IP address. See the [Cloudflare documentation] for info on 5 | //! configuring your proxy settings. However, these headers can be spoofed by clients if your origin 6 | //! server is exposed to the internet. 7 | //! 8 | //! The goal of this crate is to provide a simple extractor for clients' IP addresses whilst 9 | //! ensuring their integrity. To achieve this, it is necessary to build a list of trusted peers 10 | //! which are guaranteed to provide (e.g., Cloudflare) or pass-though (e.g., local load-balancers) 11 | //! the headers accurately. 12 | //! 13 | //! Cloudflare's trustworthy IP ranges sometimes change, so this crate also provides a utility for 14 | //! obtaining them from Cloudflare's API ([`fetch_trusted_cf_ips()`]). If your origin server's 15 | //! direct peer _is_ Cloudflare, this list will be sufficient to establish the trust chain. However, 16 | //! if your setup includes load balancers or reverse proxies then you'll need to add their IP ranges 17 | //! to the trusted set for the [`TrustedClientIp`] extractor to work as expected. 18 | //! 19 | //! # Typical Usage 20 | //! 21 | //! 1. Add an instance of [`TrustedIps`] to your app data. It is recommended to construct your 22 | //! trusted IP set using [`fetch_trusted_cf_ips()`] and add any further trusted ranges to that. 23 | //! 1. Use the [`TrustedClientIp`] extractor in your handlers. 24 | //! 25 | //! # Example 26 | //! 27 | //! ```no_run 28 | //! # async { 29 | //! # use actix_web::{App, get, HttpServer}; 30 | //! use actix_client_ip_cloudflare::{TrustedClientIp, fetch_trusted_cf_ips}; 31 | //! 32 | //! let cloudflare_ips = fetch_trusted_cf_ips() 33 | //! # // rustfmt ignore 34 | //! .await 35 | //! .unwrap() 36 | //! .add_loopback_ips(); 37 | //! 38 | //! HttpServer::new(move || { 39 | //! App::new() 40 | //! # // rustfmt ignore 41 | //! .app_data(cloudflare_ips.clone()) 42 | //! .service(handler) 43 | //! }); 44 | //! 45 | //! #[get("/")] 46 | //! async fn handler(client_ip: TrustedClientIp) -> String { 47 | //! client_ip.to_string() 48 | //! } 49 | //! # }; 50 | //! ``` 51 | //! 52 | //! # Crate Features 53 | //! 54 | //! `fetch-ips` (default): Enables functionality to (asynchronously) fetch Cloudflare's trusted IP list from 55 | //! their API. This feature includes `rustls` but if you prefer OpenSSL you can use it by disabling 56 | //! default crate features and enabling `fetch-ips-openssl` instead. 57 | //! 58 | //! [Cloudflare documentation]: https://developers.cloudflare.com/fundamentals/reference/http-request-headers 59 | 60 | #![forbid(unsafe_code)] 61 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 62 | 63 | mod extract; 64 | mod fetch_cf_ips; 65 | mod header_v4; 66 | mod header_v6; 67 | 68 | #[cfg(feature = "fetch-ips")] 69 | pub use self::fetch_cf_ips::fetch_trusted_cf_ips; 70 | pub use self::{ 71 | extract::TrustedClientIp, 72 | fetch_cf_ips::{CF_URL_IPS, TrustedIps}, 73 | header_v4::{CF_CONNECTING_IP, CfConnectingIp}, 74 | header_v6::{CF_CONNECTING_IPV6, CfConnectingIpv6}, 75 | }; 76 | -------------------------------------------------------------------------------- /actix-web-lab/src/body_channel.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use actix_web::body::{BodySize, MessageBody}; 7 | use bytes::Bytes; 8 | use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, error::SendError}; 9 | 10 | use crate::BoxError; 11 | 12 | /// Returns a sender half and a receiver half that can be used as a body type. 13 | /// 14 | /// # Examples 15 | /// ``` 16 | /// # use actix_web::{HttpResponse, web}; 17 | /// use std::convert::Infallible; 18 | /// 19 | /// use actix_web_lab::body; 20 | /// 21 | /// # async fn index() { 22 | /// let (mut body_tx, body) = body::channel::(); 23 | /// 24 | /// let _ = web::block(move || { 25 | /// body_tx 26 | /// .send(web::Bytes::from_static(b"body from another thread")) 27 | /// .unwrap(); 28 | /// }); 29 | /// 30 | /// HttpResponse::Ok().body(body) 31 | /// # ;} 32 | /// ``` 33 | pub fn channel>() -> (Sender, impl MessageBody) { 34 | let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); 35 | (Sender::new(tx), Receiver::new(rx)) 36 | } 37 | 38 | /// A channel-like sender for body chunks. 39 | #[derive(Debug, Clone)] 40 | pub struct Sender { 41 | tx: UnboundedSender>, 42 | } 43 | 44 | impl Sender { 45 | fn new(tx: UnboundedSender>) -> Self { 46 | Self { tx } 47 | } 48 | 49 | /// Submits a chunk of bytes to the response body stream. 50 | /// 51 | /// # Errors 52 | /// Errors if other side of channel body was dropped, returning `chunk`. 53 | pub fn send(&mut self, chunk: Bytes) -> Result<(), Bytes> { 54 | self.tx.send(Ok(chunk)).map_err(|SendError(err)| match err { 55 | Ok(chunk) => chunk, 56 | Err(_) => unreachable!(), 57 | }) 58 | } 59 | 60 | /// Closes the stream, optionally sending an error. 61 | /// 62 | /// # Errors 63 | /// Errors if closing with error and other side of channel body was dropped, returning `error`. 64 | pub fn close(self, error: Option) -> Result<(), E> { 65 | if let Some(err) = error { 66 | return self.tx.send(Err(err)).map_err(|SendError(err)| match err { 67 | Ok(_) => unreachable!(), 68 | Err(err) => err, 69 | }); 70 | } 71 | 72 | Ok(()) 73 | } 74 | } 75 | 76 | #[derive(Debug)] 77 | struct Receiver { 78 | rx: UnboundedReceiver>, 79 | } 80 | 81 | impl Receiver { 82 | fn new(rx: UnboundedReceiver>) -> Self { 83 | Self { rx } 84 | } 85 | } 86 | 87 | impl MessageBody for Receiver 88 | where 89 | E: Into, 90 | { 91 | type Error = E; 92 | 93 | fn size(&self) -> BodySize { 94 | BodySize::Stream 95 | } 96 | 97 | fn poll_next( 98 | mut self: Pin<&mut Self>, 99 | cx: &mut Context<'_>, 100 | ) -> Poll>> { 101 | self.rx.poll_recv(cx) 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use std::io; 108 | 109 | use super::*; 110 | 111 | static_assertions::assert_impl_all!(Sender: Send, Sync, Unpin); 112 | static_assertions::assert_impl_all!(Receiver: Send, Sync, Unpin, MessageBody); 113 | } 114 | -------------------------------------------------------------------------------- /russe/src/reqwest_0_12.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for `reqwest` v0.12. 2 | 3 | use std::io; 4 | 5 | use bytestring::ByteString; 6 | use futures_util::{StreamExt as _, TryStreamExt as _, stream::BoxStream}; 7 | use reqwest_0_12::{Client, Request, Response}; 8 | use tokio::{ 9 | sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, 10 | task::JoinHandle, 11 | }; 12 | use tokio_util::{codec::FramedRead, io::StreamReader}; 13 | 14 | use crate::{Decoder, Error, Event}; 15 | 16 | mod sealed { 17 | use super::*; 18 | 19 | pub trait Sealed {} 20 | impl Sealed for Response {} 21 | } 22 | 23 | /// SSE extension methods for `reqwest` v0.12. 24 | pub trait ReqwestExt: sealed::Sealed { 25 | /// Returns a stream of server-sent events. 26 | fn sse_stream(self) -> BoxStream<'static, Result>; 27 | } 28 | 29 | impl ReqwestExt for Response { 30 | fn sse_stream(self) -> BoxStream<'static, Result> { 31 | let body_stream = self.bytes_stream().map_err(io::Error::other); 32 | let body_reader = StreamReader::new(body_stream); 33 | 34 | let frame_reader = FramedRead::new(body_reader, Decoder::default()); 35 | 36 | Box::pin(frame_reader) 37 | } 38 | } 39 | 40 | /// An SSE request manager which tracks latest IDs and automatically reconnects. 41 | #[derive(Debug)] 42 | pub struct Manager { 43 | client: Client, 44 | req: Request, 45 | last_event_id: Option, 46 | tx: UnboundedSender>, 47 | rx: Option>>, 48 | } 49 | 50 | impl Manager { 51 | /// Constructs new SSE request manager. 52 | /// 53 | /// No attempts are made to validate or modify the given request. 54 | /// 55 | /// # Panics 56 | /// 57 | /// Panics if request is given a stream body. 58 | pub fn new(client: &Client, req: Request) -> Self { 59 | let (tx, rx) = unbounded_channel(); 60 | 61 | let req = req.try_clone().expect("Request should be clone-able"); 62 | 63 | Self { 64 | client: client.clone(), 65 | req, 66 | last_event_id: None, 67 | tx, 68 | rx: Some(rx), 69 | } 70 | } 71 | 72 | /// Sends request, starts connection management, and returns stream of events. 73 | /// 74 | /// # Panics 75 | /// 76 | /// Panics if called more than once. 77 | pub async fn send( 78 | &mut self, 79 | ) -> Result<(JoinHandle<()>, UnboundedReceiver>), Error> { 80 | let client = self.client.clone(); 81 | let req = self.req.try_clone().unwrap(); 82 | let tx = self.tx.clone(); 83 | 84 | let task_handle = tokio::spawn(async move { 85 | let mut stream = client.execute(req).await.unwrap().sse_stream(); 86 | 87 | while let Some(ev) = stream.next().await { 88 | let _ = tx.send(ev); 89 | } 90 | }); 91 | 92 | Ok((task_handle, self.rx.take().unwrap())) 93 | } 94 | 95 | /// Commits an event ID for this manager. 96 | /// 97 | /// The given ID will be used as the `Last-Event-Id` header in case of reconnects. 98 | pub fn commit_id(&mut self, id: impl Into) { 99 | self.last_event_id = Some(id.into()); 100 | } 101 | } 102 | 103 | // - optionally read id from stream and set automatically 104 | -------------------------------------------------------------------------------- /actix-web-lab/examples/sse.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates use of the Server-Sent Events (SSE) responder. 2 | 3 | use std::{convert::Infallible, io, time::Duration}; 4 | 5 | use actix_web::{App, HttpRequest, HttpServer, Responder, get, middleware::Logger, web::Html}; 6 | use actix_web_lab::{extract::Path, sse}; 7 | use futures_util::stream; 8 | use time::format_description::well_known::Rfc3339; 9 | use tokio::time::sleep; 10 | 11 | #[get("/")] 12 | async fn index() -> impl Responder { 13 | Html::new(include_str!("./assets/sse.html").to_string()) 14 | } 15 | 16 | /// Countdown event stream starting from 8. 17 | #[get("/countdown")] 18 | async fn countdown(req: HttpRequest) -> impl Responder { 19 | // note: a more production-ready implementation might want to use the lastEventId header 20 | // sent by the reconnecting browser after the _retry_ period 21 | tracing::debug!("lastEventId: {:?}", req.headers().get("Last-Event-ID")); 22 | 23 | common_countdown(8) 24 | } 25 | 26 | /// Countdown event stream with given starting number. 27 | #[get("/countdown/{n:\\d+}")] 28 | async fn countdown_from(Path(n): Path, req: HttpRequest) -> impl Responder { 29 | // note: a more production-ready implementation might want to use the lastEventId header 30 | // sent by the reconnecting browser after the _retry_ period 31 | tracing::debug!("lastEventId: {:?}", req.headers().get("Last-Event-ID")); 32 | 33 | common_countdown(n.try_into().unwrap()) 34 | } 35 | 36 | fn common_countdown(n: i32) -> impl Responder { 37 | let countdown_stream = stream::unfold((false, n), |(started, n)| async move { 38 | // allow first countdown value to yield immediately 39 | if started { 40 | sleep(Duration::from_secs(1)).await; 41 | } 42 | 43 | if n > 0 { 44 | let data = sse::Data::new(n.to_string()) 45 | .event("countdown") 46 | .id(n.to_string()); 47 | 48 | Some((Ok::<_, Infallible>(sse::Event::Data(data)), (true, n - 1))) 49 | } else { 50 | None 51 | } 52 | }); 53 | 54 | sse::Sse::from_stream(countdown_stream).with_retry_duration(Duration::from_secs(5)) 55 | } 56 | 57 | #[get("/time")] 58 | async fn timestamp() -> impl Responder { 59 | let (sender, receiver) = tokio::sync::mpsc::channel(2); 60 | 61 | actix_web::rt::spawn(async move { 62 | loop { 63 | let time = time::OffsetDateTime::now_utc(); 64 | let msg = sse::Data::new(time.format(&Rfc3339).unwrap()).event("timestamp"); 65 | 66 | if sender.send(msg.into()).await.is_err() { 67 | tracing::warn!("client disconnected; could not send SSE message"); 68 | break; 69 | } 70 | 71 | sleep(Duration::from_secs(10)).await; 72 | } 73 | }); 74 | 75 | sse::Sse::from_infallible_receiver(receiver).with_keep_alive(Duration::from_secs(3)) 76 | } 77 | 78 | #[actix_web::main] 79 | async fn main() -> io::Result<()> { 80 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 81 | 82 | tracing::info!("starting HTTP server at http://localhost:8080"); 83 | 84 | HttpServer::new(|| { 85 | App::new() 86 | .service(index) 87 | .service(countdown) 88 | .service(countdown_from) 89 | .service(timestamp) 90 | .wrap(Logger::default()) 91 | }) 92 | .workers(2) 93 | .bind(("127.0.0.1", 8080))? 94 | .run() 95 | .await 96 | } 97 | -------------------------------------------------------------------------------- /actix-web-lab/src/redirect_to_www.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | Error, Responder, 3 | body::MessageBody, 4 | dev::{ServiceRequest, ServiceResponse}, 5 | middleware::Next, 6 | web::Redirect, 7 | }; 8 | 9 | /// A function middleware to redirect traffic to `www.` if not already there. 10 | /// 11 | /// # Examples 12 | /// 13 | /// ``` 14 | /// # use actix_web::App; 15 | /// use actix_web::middleware::from_fn; 16 | /// use actix_web_lab::middleware::redirect_to_www; 17 | /// 18 | /// App::new().wrap(from_fn(redirect_to_www)) 19 | /// # ; 20 | /// ``` 21 | pub async fn redirect_to_www( 22 | req: ServiceRequest, 23 | next: Next, 24 | ) -> Result, Error> { 25 | #![allow(clippy::await_holding_refcell_ref)] // RefCell is dropped before await 26 | 27 | let (req, pl) = req.into_parts(); 28 | let conn_info = req.connection_info(); 29 | 30 | if !conn_info.host().starts_with("www.") { 31 | let scheme = conn_info.scheme(); 32 | let host = conn_info.host(); 33 | let path = req.uri().path(); 34 | let uri = format!("{scheme}://www.{host}{path}"); 35 | 36 | let res = Redirect::to(uri).respond_to(&req); 37 | 38 | drop(conn_info); 39 | return Ok(ServiceResponse::new(req, res).map_into_right_body()); 40 | } 41 | 42 | drop(conn_info); 43 | let req = ServiceRequest::from_parts(req, pl); 44 | Ok(next.call(req).await?.map_into_left_body()) 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use actix_web::{ 50 | App, HttpResponse, 51 | dev::ServiceFactory, 52 | http::{StatusCode, header}, 53 | middleware::from_fn, 54 | test, web, 55 | }; 56 | 57 | use super::*; 58 | 59 | fn test_app() -> App< 60 | impl ServiceFactory< 61 | ServiceRequest, 62 | Response = ServiceResponse, 63 | Config = (), 64 | InitError = (), 65 | Error = Error, 66 | >, 67 | > { 68 | App::new().wrap(from_fn(redirect_to_www)).route( 69 | "/", 70 | web::get().to(|| async { HttpResponse::Ok().body("content") }), 71 | ) 72 | } 73 | 74 | #[actix_web::test] 75 | async fn redirect_non_www() { 76 | let app = test::init_service(test_app()).await; 77 | 78 | let req = test::TestRequest::default().to_request(); 79 | let res = test::call_service(&app, req).await; 80 | assert_eq!(res.status(), StatusCode::TEMPORARY_REDIRECT); 81 | 82 | let loc = res.headers().get(header::LOCATION); 83 | assert!(loc.is_some()); 84 | assert!(loc.unwrap().as_bytes().starts_with(b"http://www.")); 85 | 86 | let body = test::read_body(res).await; 87 | assert!(body.is_empty()); 88 | } 89 | 90 | #[actix_web::test] 91 | async fn do_not_redirect_already_www() { 92 | let app = test::init_service(test_app()).await; 93 | 94 | let req = test::TestRequest::default() 95 | .uri("http://www.localhost/") 96 | .to_request(); 97 | let res = test::call_service(&app, req).await; 98 | assert_eq!(res.status(), StatusCode::OK); 99 | 100 | let loc = res.headers().get(header::LOCATION); 101 | assert!(loc.is_none()); 102 | 103 | let body = test::read_body(res).await; 104 | assert_eq!(body, "content"); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /actix-web-lab/src/redirect_to_non_www.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | Error, Responder, 3 | body::MessageBody, 4 | dev::{ServiceRequest, ServiceResponse}, 5 | middleware::Next, 6 | web::Redirect, 7 | }; 8 | 9 | /// A function middleware to redirect traffic away from `www.` if it's present. 10 | /// 11 | /// # Examples 12 | /// 13 | /// ``` 14 | /// # use actix_web::App; 15 | /// use actix_web::middleware::from_fn; 16 | /// use actix_web_lab::middleware::redirect_to_non_www; 17 | /// 18 | /// App::new().wrap(from_fn(redirect_to_non_www)) 19 | /// # ; 20 | /// ``` 21 | pub async fn redirect_to_non_www( 22 | req: ServiceRequest, 23 | next: Next, 24 | ) -> Result, Error> { 25 | #![allow(clippy::await_holding_refcell_ref)] // RefCell is dropped before await 26 | 27 | let (req, pl) = req.into_parts(); 28 | let conn_info = req.connection_info(); 29 | 30 | if let Some(host_no_www) = conn_info.host().strip_prefix("www.") { 31 | let scheme = conn_info.scheme(); 32 | let path = req.uri().path(); 33 | let uri = format!("{scheme}://{host_no_www}{path}"); 34 | 35 | let res = Redirect::to(uri).respond_to(&req); 36 | 37 | drop(conn_info); 38 | return Ok(ServiceResponse::new(req, res).map_into_right_body()); 39 | } 40 | 41 | drop(conn_info); 42 | let req = ServiceRequest::from_parts(req, pl); 43 | Ok(next.call(req).await?.map_into_left_body()) 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use actix_web::{ 49 | App, HttpResponse, 50 | dev::ServiceFactory, 51 | http::{StatusCode, header}, 52 | middleware::from_fn, 53 | test, web, 54 | }; 55 | 56 | use super::*; 57 | 58 | fn test_app() -> App< 59 | impl ServiceFactory< 60 | ServiceRequest, 61 | Response = ServiceResponse, 62 | Config = (), 63 | InitError = (), 64 | Error = Error, 65 | >, 66 | > { 67 | App::new().wrap(from_fn(redirect_to_non_www)).route( 68 | "/", 69 | web::get().to(|| async { HttpResponse::Ok().body("content") }), 70 | ) 71 | } 72 | 73 | #[actix_web::test] 74 | async fn redirect_non_www() { 75 | let app = test::init_service(test_app()).await; 76 | 77 | let req = test::TestRequest::get() 78 | .uri("http://www.localhost/") 79 | .to_request(); 80 | let res = test::call_service(&app, req).await; 81 | assert_eq!(res.status(), StatusCode::TEMPORARY_REDIRECT); 82 | 83 | let loc = res.headers().get(header::LOCATION); 84 | assert!(loc.is_some()); 85 | assert!(!loc.unwrap().as_bytes().starts_with(b"http://www.")); 86 | 87 | let body = test::read_body(res).await; 88 | assert!(body.is_empty()); 89 | } 90 | 91 | #[actix_web::test] 92 | async fn do_not_redirect_already_non_www() { 93 | let app = test::init_service(test_app()).await; 94 | 95 | let req = test::TestRequest::default() 96 | .uri("http://localhost/") 97 | .to_request(); 98 | let res = test::call_service(&app, req).await; 99 | assert_eq!(res.status(), StatusCode::OK); 100 | 101 | let loc = res.headers().get(header::LOCATION); 102 | assert!(loc.is_none()); 103 | 104 | let body = test::read_body(res).await; 105 | assert_eq!(body, "content"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /actix-web-lab/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-lab" 3 | version = "0.24.3" 4 | description = "In-progress extractors and middleware for Actix Web" 5 | authors = ["Rob Ede "] 6 | keywords = ["actix", "http", "web", "framework", "async"] 7 | categories = [ 8 | "network-programming", 9 | "asynchronous", 10 | "web-programming::http-server", 11 | "web-programming::websocket", 12 | ] 13 | repository.workspace = true 14 | license.workspace = true 15 | edition.workspace = true 16 | rust-version.workspace = true 17 | 18 | [package.metadata.docs.rs] 19 | all-features = true 20 | rustdoc-args = ["--cfg", "docsrs"] 21 | 22 | [package.metadata.cargo_check_external_types] 23 | allowed_external_types = [ 24 | "actix_http::*", 25 | "actix_service::*", 26 | "actix_utils::*", 27 | "actix_web_lab_derive::*", 28 | "actix_web::*", 29 | "arc_swap::*", 30 | "bytes::*", 31 | "bytestring::*", 32 | "futures_core::*", 33 | "http::*", 34 | "mime::*", 35 | "serde_json::*", 36 | "serde::*", 37 | "tokio::*", 38 | ] 39 | 40 | [features] 41 | default = ["derive"] 42 | derive = ["actix-web-lab-derive"] 43 | 44 | cbor = ["serde_cbor_2"] 45 | msgpack = ["rmp-serde"] 46 | spa = ["actix-files"] 47 | 48 | [dependencies] 49 | actix-web-lab-derive = { version = "0.24", optional = true } 50 | 51 | actix-http = "3.10" 52 | actix-router = "0.5" 53 | actix-service = "2" 54 | actix-utils = "3" 55 | actix-web = { version = "4.9", default-features = false } 56 | ahash = "0.8" 57 | arc-swap = "1.1" 58 | bytes = "1" 59 | bytestring = "1" 60 | csv = "1.1" 61 | derive_more = { version = "2", features = ["display", "error"] } 62 | form_urlencoded = "1" 63 | futures-core = "0.3.17" 64 | futures-util = { version = "0.3.31", default-features = false, features = ["std"] } 65 | http = "0.2.7" 66 | impl-more = "0.3" 67 | itertools = "0.14" 68 | local-channel = "0.1" 69 | mime = "0.3" 70 | pin-project-lite = "0.2.16" 71 | regex = "1.11.0" 72 | serde = "1" 73 | serde_html_form = "0.2" 74 | serde_json = "1" 75 | serde_path_to_error = "0.1" 76 | tokio = { version = "1.38.2", features = ["sync", "macros"] } 77 | tokio-stream = "0.1.17" 78 | tracing = { version = "0.1.41", features = ["log"] } 79 | url = "2.1" 80 | 81 | # cbor 82 | serde_cbor_2 = { version = "0.12.0-dev", optional = true } 83 | 84 | # msgpack 85 | rmp-serde = { version = "1", optional = true } 86 | 87 | # spa 88 | actix-files = { version = "0.6", optional = true } 89 | 90 | [dev-dependencies] 91 | actix-web-lab-derive = "0.24" 92 | 93 | actix-web = { version = "4", features = ["rustls-0_23"] } 94 | async_zip = { version = "0.0.17", features = ["deflate", "tokio"] } 95 | base64 = "0.22" 96 | digest = "0.10" 97 | ed25519-dalek = { version = "2.2", features = ["hazmat"] } 98 | env_logger = "0.11" 99 | futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] } 100 | generic-array = "0.14" 101 | hex = "0.4" 102 | hex-literal = "0.4" 103 | hmac = { version = "0.12", features = ["reset"] } 104 | rand = "0.9" 105 | rustls = "0.23" 106 | rustls-pemfile = "2" 107 | serde = { version = "1", features = ["derive"] } 108 | sha2 = "0.10" 109 | static_assertions = "1.1" 110 | time = { version = "0.3", features = ["formatting"] } 111 | tokio = { version = "1.38.2", features = ["full"] } 112 | tokio-util = { version = "0.7", features = ["compat"] } 113 | 114 | [[example]] 115 | name = "cbor" 116 | required-features = ["cbor"] 117 | 118 | [[example]] 119 | name = "msgpack" 120 | required-features = ["msgpack"] 121 | 122 | [[example]] 123 | name = "spa" 124 | required-features = ["spa"] 125 | 126 | [lints] 127 | workspace = true 128 | -------------------------------------------------------------------------------- /actix-web-lab/examples/ndjson.rs: -------------------------------------------------------------------------------- 1 | //! How to use `NdJson` as an efficient streaming response type. 2 | //! 3 | //! The same techniques can also be used for `Csv`. 4 | //! 5 | //! Select the number of NDJSON items to return using the query string. Example: `/users?n=100`. 6 | //! 7 | //! Also includes a low-efficiency route to demonstrate the difference. 8 | 9 | use std::io::{self, Write as _}; 10 | 11 | use actix_web::{ 12 | App, HttpResponse, HttpServer, Responder, get, 13 | web::{self, BufMut as _, BytesMut}, 14 | }; 15 | use actix_web_lab::respond::NdJson; 16 | use futures_core::Stream; 17 | use futures_util::{StreamExt as _, stream}; 18 | use rand::{ 19 | Rng as _, 20 | distr::{Alphanumeric, SampleString as _}, 21 | }; 22 | use serde::Deserialize; 23 | use serde_json::json; 24 | use tracing::info; 25 | 26 | fn streaming_data_source(n: u32) -> impl Stream> { 27 | stream::repeat_with(|| { 28 | Ok(json!({ 29 | "email": random_email(), 30 | "address": random_address(), 31 | })) 32 | }) 33 | .take(n as usize) 34 | } 35 | 36 | #[derive(Debug, Deserialize)] 37 | struct Opts { 38 | n: Option, 39 | } 40 | 41 | /// This handler streams data as NDJSON to the client in a fast and memory efficient way. 42 | /// 43 | /// A real data source might be a downstream server, database query, or other external resource. 44 | #[get("/users")] 45 | async fn get_user_list(opts: web::Query) -> impl Responder { 46 | let n_items = opts.n.unwrap_or(10); 47 | let data_stream = streaming_data_source(n_items); 48 | 49 | NdJson::new(data_stream) 50 | .into_responder() 51 | .customize() 52 | .insert_header(("num-results", n_items)) 53 | 54 | // alternative if you need more control of the HttpResponse: 55 | // 56 | // HttpResponse::Ok() 57 | // .insert_header(("content-type", NdJson::mime())) 58 | // .insert_header(("num-results", n_items)) 59 | // .body(NdJson::new(data_stream).into_body_stream()) 60 | } 61 | 62 | /// A comparison route that loads all the data into memory before sending it to the client. 63 | /// 64 | /// If you provide a high number in the query string like `?n=300000` you should be able to observe 65 | /// increasing memory usage of the process in your process monitor. 66 | #[get("/users-high-mem")] 67 | async fn get_high_mem_user_list(opts: web::Query) -> impl Responder { 68 | let n_items = opts.n.unwrap_or(10); 69 | let mut stream = streaming_data_source(n_items); 70 | 71 | // buffer all data from the source into a Bytes container 72 | let mut buf = BytesMut::new().writer(); 73 | while let Some(Ok(item)) = stream.next().await { 74 | serde_json::to_writer(&mut buf, &item).unwrap(); 75 | buf.write_all(b"\n").unwrap(); 76 | } 77 | 78 | HttpResponse::Ok() 79 | .insert_header(("content-type", NdJson::mime())) 80 | .insert_header(("num-results", n_items)) 81 | .body(buf.into_inner()) 82 | } 83 | 84 | #[actix_web::main] 85 | async fn main() -> io::Result<()> { 86 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 87 | 88 | let bind = ("127.0.0.1", 8080); 89 | info!("staring server at http://{}:{}", &bind.0, &bind.1); 90 | 91 | HttpServer::new(|| { 92 | App::new() 93 | .service(get_user_list) 94 | .service(get_high_mem_user_list) 95 | }) 96 | .workers(1) 97 | .bind(bind)? 98 | .run() 99 | .await 100 | } 101 | 102 | fn random_email() -> String { 103 | let id = Alphanumeric.sample_string(&mut rand::rng(), 10); 104 | format!("user_{id}@example.com") 105 | } 106 | 107 | fn random_address() -> String { 108 | let street_no: u16 = rand::rng().random_range(10..=99); 109 | format!("{street_no} Random Street") 110 | } 111 | -------------------------------------------------------------------------------- /actix-web-lab/src/header.rs: -------------------------------------------------------------------------------- 1 | //! Experimental typed headers. 2 | 3 | use std::{fmt, str::FromStr}; 4 | 5 | use actix_http::{error::ParseError, header::HeaderValue}; 6 | 7 | #[cfg(test)] 8 | pub(crate) use self::header_test_helpers::{assert_parse_eq, assert_parse_fail}; 9 | pub use crate::{ 10 | cache_control::{CacheControl, CacheDirective}, 11 | clear_site_data::{ClearSiteData, ClearSiteDataDirective}, 12 | content_length::ContentLength, 13 | forwarded::Forwarded, 14 | strict_transport_security::StrictTransportSecurity, 15 | x_forwarded_prefix::{X_FORWARDED_PREFIX, XForwardedPrefix}, 16 | }; 17 | 18 | /// Parses a group of comma-delimited quoted-string headers. 19 | /// 20 | /// Notes that `T`'s [`FromStr`] implementation SHOULD NOT try to strip leading or trailing quotes 21 | /// when parsing (or try to enforce them), since the quoted-string grammar itself enforces them and 22 | /// so this function checks for their existence, and strips them before passing to [`FromStr`]. 23 | #[inline] 24 | pub(crate) fn from_comma_delimited_quoted_strings<'a, I, T>(all: I) -> Result, ParseError> 25 | where 26 | I: Iterator + 'a, 27 | T: FromStr, 28 | { 29 | let size_guess = all.size_hint().1.unwrap_or(2); 30 | let mut result = Vec::with_capacity(size_guess); 31 | 32 | for hdr in all { 33 | let hdr_str = hdr.to_str().map_err(|_| ParseError::Header)?; 34 | 35 | for part in hdr_str.split(',').filter_map(|x| match x.trim() { 36 | "" => None, 37 | y => Some(y), 38 | }) { 39 | if let Ok(part) = part 40 | .strip_prefix('"') 41 | .and_then(|part| part.strip_suffix('"')) 42 | // reject headers which are not properly quoted-string formatted 43 | .ok_or(ParseError::Header)? 44 | .parse() 45 | { 46 | result.push(part); 47 | } 48 | } 49 | } 50 | 51 | Ok(result) 52 | } 53 | 54 | /// Formats a list of headers into a comma-delimited quoted-string string. 55 | #[inline] 56 | pub(crate) fn fmt_comma_delimited_quoted_strings<'a, I, T>( 57 | f: &mut fmt::Formatter<'_>, 58 | mut parts: I, 59 | ) -> fmt::Result 60 | where 61 | I: Iterator + 'a, 62 | T: 'a + fmt::Display, 63 | { 64 | let Some(part) = parts.next() else { 65 | return Ok(()); 66 | }; 67 | 68 | write!(f, "\"{part}\"")?; 69 | 70 | for part in parts { 71 | write!(f, ", \"{part}\"")?; 72 | } 73 | 74 | Ok(()) 75 | } 76 | 77 | #[cfg(test)] 78 | mod header_test_helpers { 79 | use std::fmt; 80 | 81 | use actix_http::header::Header; 82 | use actix_web::{HttpRequest, test}; 83 | 84 | fn req_from_raw_headers, V: AsRef<[u8]>>( 85 | header_lines: I, 86 | ) -> HttpRequest { 87 | header_lines 88 | .into_iter() 89 | .fold(test::TestRequest::default(), |req, item| { 90 | req.append_header((H::name(), item.as_ref().to_vec())) 91 | }) 92 | .to_http_request() 93 | } 94 | 95 | #[track_caller] 96 | pub(crate) fn assert_parse_eq< 97 | H: Header + fmt::Debug + PartialEq, 98 | I: IntoIterator, 99 | V: AsRef<[u8]>, 100 | >( 101 | headers: I, 102 | expect: H, 103 | ) { 104 | let req = req_from_raw_headers::(headers); 105 | assert_eq!(H::parse(&req).unwrap(), expect); 106 | } 107 | 108 | #[track_caller] 109 | pub(crate) fn assert_parse_fail< 110 | H: Header + fmt::Debug, 111 | I: IntoIterator, 112 | V: AsRef<[u8]>, 113 | >( 114 | headers: I, 115 | ) { 116 | let req = req_from_raw_headers::(headers); 117 | H::parse(&req).unwrap_err(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /actix-web-lab/src/display_stream.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error as StdError, fmt, io::Write as _}; 2 | 3 | use actix_web::{ 4 | HttpResponse, Responder, 5 | body::{BodyStream, MessageBody}, 6 | }; 7 | use bytes::{Bytes, BytesMut}; 8 | use futures_core::Stream; 9 | use futures_util::TryStreamExt as _; 10 | use pin_project_lite::pin_project; 11 | 12 | use crate::util::{InfallibleStream, MutWriter}; 13 | 14 | pin_project! { 15 | /// A buffered line formatting body stream. 16 | /// 17 | /// Each item yielded by the stream will be written to the response body using its 18 | /// `Display` implementation. 19 | /// 20 | /// This has significant memory efficiency advantages over returning an array of lines when the 21 | /// data set is very large because it avoids buffering the entire response. 22 | /// 23 | /// # Examples 24 | /// ``` 25 | /// # use actix_web::Responder; 26 | /// # use actix_web_lab::respond::DisplayStream; 27 | /// # use futures_core::Stream; 28 | /// fn streaming_data_source() -> impl Stream { 29 | /// // get item stream from source 30 | /// # futures_util::stream::empty() 31 | /// } 32 | /// 33 | /// async fn handler() -> impl Responder { 34 | /// let data_stream = streaming_data_source(); 35 | /// 36 | /// DisplayStream::new_infallible(data_stream) 37 | /// .into_responder() 38 | /// } 39 | /// ``` 40 | pub struct DisplayStream { 41 | // The wrapped item stream. 42 | #[pin] 43 | stream: S, 44 | } 45 | } 46 | 47 | impl DisplayStream { 48 | /// Constructs a new `DisplayStream` from a stream of lines. 49 | pub fn new(stream: S) -> Self { 50 | Self { stream } 51 | } 52 | } 53 | 54 | impl DisplayStream { 55 | /// Constructs a new `DisplayStream` from an infallible stream of lines. 56 | pub fn new_infallible(stream: S) -> DisplayStream> { 57 | DisplayStream::new(InfallibleStream::new(stream)) 58 | } 59 | } 60 | 61 | impl DisplayStream 62 | where 63 | S: Stream>, 64 | T: fmt::Display, 65 | E: Into> + 'static, 66 | { 67 | /// Creates a chunked body stream that serializes as CSV on-the-fly. 68 | pub fn into_body_stream(self) -> impl MessageBody { 69 | BodyStream::new(self.into_chunk_stream()) 70 | } 71 | 72 | /// Creates a `Responder` type with a line-by-line serializing stream and `text/plain` 73 | /// content-type header. 74 | pub fn into_responder(self) -> impl Responder 75 | where 76 | S: 'static, 77 | T: 'static, 78 | E: 'static, 79 | { 80 | HttpResponse::Ok() 81 | .content_type(mime::TEXT_PLAIN_UTF_8) 82 | .message_body(self.into_body_stream()) 83 | .unwrap() 84 | } 85 | 86 | /// Creates a stream of serialized chunks. 87 | pub fn into_chunk_stream(self) -> impl Stream> { 88 | self.stream.map_ok(write_display) 89 | } 90 | } 91 | 92 | fn write_display(item: impl fmt::Display) -> Bytes { 93 | let mut buf = BytesMut::new(); 94 | let mut wrt = MutWriter(&mut buf); 95 | 96 | writeln!(wrt, "{item}").unwrap(); 97 | 98 | buf.freeze() 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use std::error::Error as StdError; 104 | 105 | use actix_web::body; 106 | use futures_util::stream; 107 | 108 | use super::*; 109 | 110 | #[actix_web::test] 111 | async fn serializes_into_body() { 112 | let ndjson_body = DisplayStream::new_infallible(stream::iter([123, 789, 345, 901, 456])) 113 | .into_body_stream(); 114 | 115 | let body_bytes = body::to_bytes(ndjson_body) 116 | .await 117 | .map_err(Into::>::into) 118 | .unwrap(); 119 | 120 | const EXP_BYTES: &str = "123\n\ 121 | 789\n\ 122 | 345\n\ 123 | 901\n\ 124 | 456\n"; 125 | 126 | assert_eq!(body_bytes, EXP_BYTES); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /actix-client-ip-cloudflare/src/extract.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use actix_utils::future::{Ready, err, ok}; 4 | use actix_web::{ 5 | FromRequest, HttpRequest, 6 | dev::{self, PeerAddr}, 7 | http::header::Header as _, 8 | }; 9 | 10 | use crate::{CfConnectingIp, CfConnectingIpv6, fetch_cf_ips::TrustedIps}; 11 | 12 | fn bad_req(err: impl Into) -> actix_web::error::Error { 13 | actix_web::error::ErrorBadRequest(format!("TrustedClientIp error: {}", err.into())) 14 | } 15 | 16 | /// Extractor for a client IP that has passed through Cloudflare and is verified as not spoofed. 17 | /// 18 | /// For this extractor to work, there must be an instance of [`TrustedIps`] in your app data. 19 | #[derive(Debug, Clone)] 20 | pub struct TrustedClientIp(pub IpAddr); 21 | 22 | impl_more::forward_display!(TrustedClientIp); 23 | 24 | impl FromRequest for TrustedClientIp { 25 | type Error = actix_web::Error; 26 | type Future = Ready>; 27 | 28 | fn from_request(req: &HttpRequest, _pl: &mut dev::Payload) -> Self::Future { 29 | let client_ip_hdr = match CfConnectingIp::parse(req) { 30 | Ok(ip) => Ok(ip.ip()), 31 | Err(_) => Err(()), 32 | }; 33 | 34 | let client_ipv6_hdr = match CfConnectingIpv6::parse(req) { 35 | Ok(ip) => Ok(ip.ip()), 36 | Err(_) => Err(()), 37 | }; 38 | 39 | let client_ip = match client_ip_hdr.or(client_ipv6_hdr) { 40 | Ok(ip) => ip, 41 | Err(_) => return err(bad_req("cf-connecting-ip header not present")), 42 | }; 43 | 44 | let trusted_ips = match req.app_data::() { 45 | Some(ips) => ips, 46 | None => return err(bad_req("trusted IPs not in app data")), 47 | }; 48 | 49 | let peer_ip = PeerAddr::extract(req).into_inner().unwrap().0.ip(); 50 | 51 | if trusted_ips.contains(peer_ip) { 52 | ok(Self(client_ip)) 53 | } else { 54 | err(bad_req("cf-connecting-ip read from untrusted peer")) 55 | } 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use actix_web::test::TestRequest; 62 | 63 | use super::*; 64 | 65 | fn sample_trusted_ips() -> TrustedIps { 66 | TrustedIps { 67 | cidr_ranges: Vec::from([ 68 | "103.21.244.0/22".parse().unwrap(), 69 | "198.41.128.0/17".parse().unwrap(), 70 | ]), 71 | } 72 | } 73 | 74 | #[test] 75 | fn missing_app_data() { 76 | let req = TestRequest::default() 77 | .insert_header(("CF-Connecting-IP", "4.5.6.7")) 78 | .to_http_request(); 79 | 80 | TrustedClientIp::extract(&req).into_inner().unwrap_err(); 81 | } 82 | 83 | #[test] 84 | fn from_untrusted_peer() { 85 | let trusted_ips = sample_trusted_ips(); 86 | 87 | let req = TestRequest::default() 88 | .insert_header(("CF-Connecting-IP", "4.5.6.7")) 89 | .peer_addr("10.0.1.1:27432".parse().unwrap()) 90 | .app_data(trusted_ips) 91 | .to_http_request(); 92 | 93 | TrustedClientIp::extract(&req).into_inner().unwrap_err(); 94 | } 95 | 96 | #[test] 97 | fn from_trusted_peer() { 98 | let trusted_ips = sample_trusted_ips(); 99 | 100 | let req = TestRequest::default() 101 | .insert_header(("CF-Connecting-IP", "4.5.6.7")) 102 | .peer_addr("103.21.244.0:27432".parse().unwrap()) 103 | .app_data(trusted_ips) 104 | .to_http_request(); 105 | 106 | TrustedClientIp::extract(&req).into_inner().unwrap(); 107 | } 108 | 109 | #[test] 110 | fn from_additional_trusted_peer() { 111 | let trusted_ips = sample_trusted_ips().add_ip_range("10.0.1.0/24".parse().unwrap()); 112 | 113 | let req = TestRequest::default() 114 | .insert_header(("CF-Connecting-IP", "4.5.6.7")) 115 | .peer_addr("10.0.1.1:27432".parse().unwrap()) 116 | .app_data(trusted_ips) 117 | .to_http_request(); 118 | 119 | TrustedClientIp::extract(&req).into_inner().unwrap(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /actix-web-lab/examples/from_fn.rs: -------------------------------------------------------------------------------- 1 | //! Shows a couple of ways to use the `from_fn` middleware. 2 | 3 | use std::{collections::HashMap, io, rc::Rc, time::Duration}; 4 | 5 | use actix_web::{ 6 | App, Error, HttpResponse, HttpServer, 7 | body::MessageBody, 8 | dev::{Service, ServiceRequest, ServiceResponse, Transform}, 9 | http::header::{self, HeaderValue, Range}, 10 | middleware::{Logger, Next, from_fn}, 11 | web::{self, Header, Query}, 12 | }; 13 | use tracing::info; 14 | 15 | async fn noop(req: ServiceRequest, next: Next) -> Result, Error> { 16 | next.call(req).await 17 | } 18 | 19 | async fn print_range_header( 20 | range_header: Option>, 21 | req: ServiceRequest, 22 | next: Next, 23 | ) -> Result, Error> { 24 | if let Some(Header(range)) = range_header { 25 | println!("Range: {range}"); 26 | } else { 27 | println!("No Range header"); 28 | } 29 | 30 | next.call(req).await 31 | } 32 | 33 | async fn mutate_body_type( 34 | req: ServiceRequest, 35 | next: Next, 36 | ) -> Result, Error> { 37 | let res = next.call(req).await?; 38 | Ok(res.map_into_left_body::<()>()) 39 | } 40 | 41 | async fn mutate_body_type_with_extractors( 42 | string_body: String, 43 | query: Query>, 44 | req: ServiceRequest, 45 | next: Next, 46 | ) -> Result, Error> { 47 | println!("body is: {string_body}"); 48 | println!("query string: {query:?}"); 49 | 50 | let res = next.call(req).await?; 51 | 52 | Ok(res.map_body(move |_, _| string_body)) 53 | } 54 | 55 | async fn timeout_10secs( 56 | req: ServiceRequest, 57 | next: Next, 58 | ) -> Result, Error> { 59 | match tokio::time::timeout(Duration::from_secs(10), next.call(req)).await { 60 | Ok(res) => res, 61 | Err(_err) => Err(actix_web::error::ErrorRequestTimeout("")), 62 | } 63 | } 64 | 65 | struct MyMw(bool); 66 | 67 | impl MyMw { 68 | async fn mw_cb( 69 | &self, 70 | req: ServiceRequest, 71 | next: Next, 72 | ) -> Result>, Error> { 73 | let mut res = match self.0 { 74 | true => req.into_response("short-circuited").map_into_right_body(), 75 | false => next.call(req).await?.map_into_left_body(), 76 | }; 77 | 78 | res.headers_mut() 79 | .insert(header::WARNING, HeaderValue::from_static("42")); 80 | 81 | Ok(res) 82 | } 83 | 84 | pub fn into_middleware( 85 | self, 86 | ) -> impl Transform< 87 | S, 88 | ServiceRequest, 89 | Response = ServiceResponse, 90 | Error = Error, 91 | InitError = (), 92 | > 93 | where 94 | S: Service, Error = Error> + 'static, 95 | B: MessageBody + 'static, 96 | { 97 | let this = Rc::new(self); 98 | from_fn(move |req, next| { 99 | let this = Rc::clone(&this); 100 | async move { Self::mw_cb(&this, req, next).await } 101 | }) 102 | } 103 | } 104 | 105 | #[actix_web::main] 106 | async fn main() -> io::Result<()> { 107 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 108 | 109 | let bind = ("127.0.0.1", 8080); 110 | info!("staring server at http://{}:{}", &bind.0, &bind.1); 111 | 112 | HttpServer::new(|| { 113 | App::new() 114 | .wrap(from_fn(noop)) 115 | .wrap(from_fn(print_range_header)) 116 | .wrap(from_fn(mutate_body_type)) 117 | .wrap(from_fn(mutate_body_type_with_extractors)) 118 | .wrap(from_fn(timeout_10secs)) 119 | // switch bool to true to observe early response 120 | .wrap(MyMw(false).into_middleware()) 121 | .wrap(Logger::default()) 122 | .default_service(web::to(HttpResponse::Ok)) 123 | }) 124 | .workers(1) 125 | .bind(bind)? 126 | .run() 127 | .await 128 | } 129 | -------------------------------------------------------------------------------- /actix-web-lab/src/csv.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, error::Error as StdError}; 2 | 3 | use actix_web::{ 4 | HttpResponse, Responder, 5 | body::{BodyStream, MessageBody}, 6 | }; 7 | use bytes::{Bytes, BytesMut}; 8 | use futures_core::Stream; 9 | use futures_util::TryStreamExt as _; 10 | use mime::Mime; 11 | use pin_project_lite::pin_project; 12 | use serde::Serialize; 13 | 14 | use crate::util::{InfallibleStream, MutWriter}; 15 | 16 | pin_project! { 17 | /// A buffered CSV serializing body stream. 18 | /// 19 | /// This has significant memory efficiency advantages over returning an array of CSV rows when 20 | /// the data set is very large because it avoids buffering the entire response. 21 | /// 22 | /// # Examples 23 | /// ``` 24 | /// # use actix_web::Responder; 25 | /// # use actix_web_lab::respond::Csv; 26 | /// # use futures_core::Stream; 27 | /// fn streaming_data_source() -> impl Stream { 28 | /// // get item stream from source 29 | /// # futures_util::stream::empty() 30 | /// } 31 | /// 32 | /// async fn handler() -> impl Responder { 33 | /// let data_stream = streaming_data_source(); 34 | /// 35 | /// Csv::new_infallible(data_stream) 36 | /// .into_responder() 37 | /// } 38 | /// ``` 39 | pub struct Csv { 40 | // The wrapped item stream. 41 | #[pin] 42 | stream: S, 43 | } 44 | } 45 | 46 | impl Csv { 47 | /// Constructs a new `Csv` from a stream of rows. 48 | pub fn new(stream: S) -> Self { 49 | Self { stream } 50 | } 51 | } 52 | 53 | impl Csv { 54 | /// Constructs a new `Csv` from an infallible stream of rows. 55 | pub fn new_infallible(stream: S) -> Csv> { 56 | Csv::new(InfallibleStream::new(stream)) 57 | } 58 | } 59 | 60 | impl Csv 61 | where 62 | S: Stream>, 63 | T: Serialize, 64 | E: Into> + 'static, 65 | { 66 | /// Creates a chunked body stream that serializes as CSV on-the-fly. 67 | pub fn into_body_stream(self) -> impl MessageBody { 68 | BodyStream::new(self.into_chunk_stream()) 69 | } 70 | 71 | /// Creates a `Responder` type with a serializing stream and correct `Content-Type` header. 72 | pub fn into_responder(self) -> impl Responder 73 | where 74 | S: 'static, 75 | T: 'static, 76 | E: 'static, 77 | { 78 | HttpResponse::Ok() 79 | .content_type(mime::TEXT_CSV_UTF_8) 80 | .message_body(self.into_body_stream()) 81 | .unwrap() 82 | } 83 | 84 | /// Creates a stream of serialized chunks. 85 | pub fn into_chunk_stream(self) -> impl Stream> { 86 | self.stream.map_ok(serialize_csv_row) 87 | } 88 | } 89 | 90 | impl Csv { 91 | /// Returns the CSV MIME type (`text/csv; charset=utf-8`). 92 | pub fn mime() -> Mime { 93 | mime::TEXT_CSV_UTF_8 94 | } 95 | } 96 | 97 | fn serialize_csv_row(item: impl Serialize) -> Bytes { 98 | let mut buf = BytesMut::new(); 99 | let wrt = MutWriter(&mut buf); 100 | 101 | // serialize CSV row to buffer 102 | let mut csv_wrt = csv::Writer::from_writer(wrt); 103 | csv_wrt.serialize(&item).unwrap(); 104 | csv_wrt.flush().unwrap(); 105 | 106 | drop(csv_wrt); 107 | buf.freeze() 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use std::error::Error as StdError; 113 | 114 | use actix_web::body; 115 | use futures_util::stream; 116 | 117 | use super::*; 118 | 119 | #[actix_web::test] 120 | async fn serializes_into_body() { 121 | let ndjson_body = Csv::new_infallible(stream::iter([ 122 | [123, 456], 123 | [789, 12], 124 | [345, 678], 125 | [901, 234], 126 | [456, 789], 127 | ])) 128 | .into_body_stream(); 129 | 130 | let body_bytes = body::to_bytes(ndjson_body) 131 | .await 132 | .map_err(Into::>::into) 133 | .unwrap(); 134 | 135 | const EXP_BYTES: &str = "123,456\n\ 136 | 789,12\n\ 137 | 345,678\n\ 138 | 901,234\n\ 139 | 456,789\n"; 140 | 141 | assert_eq!(body_bytes, EXP_BYTES); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /actix-web-lab/src/condition_option.rs: -------------------------------------------------------------------------------- 1 | //! For middleware documentation, see [`ConditionOption`]. 2 | 3 | use std::{ 4 | pin::Pin, 5 | task::{self, Poll, ready}, 6 | }; 7 | 8 | use actix_web::{ 9 | body::EitherBody, 10 | dev::{Service, ServiceResponse, Transform}, 11 | }; 12 | use futures_core::future::LocalBoxFuture; 13 | use futures_util::future::FutureExt as _; 14 | use pin_project_lite::pin_project; 15 | 16 | /// Middleware for conditionally enabling other middleware in an [`Option`]. 17 | /// 18 | /// # Example 19 | /// ``` 20 | /// use actix_web::{App, middleware::Logger}; 21 | /// use actix_web_lab::middleware::ConditionOption; 22 | /// 23 | /// let normalize: ConditionOption<_> = Some(Logger::default()).into(); 24 | /// let app = App::new().wrap(normalize); 25 | /// ``` 26 | #[derive(Debug)] 27 | pub struct ConditionOption { 28 | inner: Option, 29 | } 30 | 31 | impl From> for ConditionOption { 32 | fn from(value: Option) -> Self { 33 | Self { inner: value } 34 | } 35 | } 36 | 37 | impl Transform for ConditionOption 38 | where 39 | S: Service, Error = Err> + 'static, 40 | T: Transform, Error = Err>, 41 | T::Future: 'static, 42 | T::InitError: 'static, 43 | T::Transform: 'static, 44 | { 45 | type Response = ServiceResponse>; 46 | type Error = Err; 47 | type Transform = ConditionMiddleware; 48 | type InitError = T::InitError; 49 | type Future = LocalBoxFuture<'static, Result>; 50 | 51 | fn new_transform(&self, service: S) -> Self::Future { 52 | match &self.inner { 53 | Some(transformer) => { 54 | let fut = transformer.new_transform(service); 55 | async move { 56 | let wrapped_svc = fut.await?; 57 | Ok(ConditionMiddleware::Enable(wrapped_svc)) 58 | } 59 | .boxed_local() 60 | } 61 | None => async move { Ok(ConditionMiddleware::Disable(service)) }.boxed_local(), 62 | } 63 | } 64 | } 65 | 66 | /// TODO 67 | #[derive(Debug)] 68 | pub enum ConditionMiddleware { 69 | Enable(E), 70 | Disable(D), 71 | } 72 | 73 | impl Service for ConditionMiddleware 74 | where 75 | E: Service, Error = Err>, 76 | D: Service, Error = Err>, 77 | { 78 | type Response = ServiceResponse>; 79 | type Error = Err; 80 | type Future = ConditionMiddlewareFuture; 81 | 82 | fn poll_ready(&self, cx: &mut task::Context<'_>) -> Poll> { 83 | match self { 84 | ConditionMiddleware::Enable(service) => service.poll_ready(cx), 85 | ConditionMiddleware::Disable(service) => service.poll_ready(cx), 86 | } 87 | } 88 | 89 | fn call(&self, req: Req) -> Self::Future { 90 | match self { 91 | ConditionMiddleware::Enable(service) => ConditionMiddlewareFuture::Enabled { 92 | fut: service.call(req), 93 | }, 94 | ConditionMiddleware::Disable(service) => ConditionMiddlewareFuture::Disabled { 95 | fut: service.call(req), 96 | }, 97 | } 98 | } 99 | } 100 | 101 | pin_project! { 102 | #[doc(hidden)] 103 | #[project = ConditionProj] 104 | pub enum ConditionMiddlewareFuture { 105 | Enabled { #[pin] fut: E, }, 106 | Disabled { #[pin] fut: D, }, 107 | } 108 | } 109 | 110 | impl Future for ConditionMiddlewareFuture 111 | where 112 | E: Future, Err>>, 113 | D: Future, Err>>, 114 | { 115 | type Output = Result>, Err>; 116 | 117 | #[inline] 118 | fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { 119 | let res = match self.project() { 120 | ConditionProj::Enabled { fut } => ready!(fut.poll(cx))?.map_into_left_body(), 121 | ConditionProj::Disabled { fut } => ready!(fut.poll(cx))?.map_into_right_body(), 122 | }; 123 | 124 | Poll::Ready(Ok(res)) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /actix-web-lab/src/swap_data.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use actix_utils::future::{Ready, ready}; 4 | use actix_web::{Error, FromRequest, HttpRequest, dev, error}; 5 | use arc_swap::{ArcSwap, Guard}; 6 | use tracing::debug; 7 | 8 | /// A wrapper around `ArcSwap` that can be used as an extractor. 9 | /// 10 | /// Can serve as a replacement for `Data>` in certain situations. 11 | /// 12 | /// Currently exposes some internals of `arc-swap` and may change in the future. 13 | #[derive(Debug)] 14 | pub struct SwapData { 15 | swap: Arc>, 16 | } 17 | 18 | impl SwapData { 19 | /// Constructs new swappable data item. 20 | pub fn new(item: T) -> Self { 21 | Self { 22 | swap: Arc::new(ArcSwap::new(Arc::new(item))), 23 | } 24 | } 25 | 26 | /// Returns a temporary access guard to the wrapped data item. 27 | /// 28 | /// Implements `Deref` for read access to the inner data item. 29 | pub fn load(&self) -> Guard> { 30 | self.swap.load() 31 | } 32 | 33 | /// Replaces the value inside this instance. 34 | /// 35 | /// Further `load`s will yield the new value. 36 | pub fn store(&self, item: T) { 37 | self.swap.store(Arc::new(item)) 38 | } 39 | } 40 | 41 | impl Clone for SwapData { 42 | fn clone(&self) -> Self { 43 | Self { 44 | swap: Arc::clone(&self.swap), 45 | } 46 | } 47 | } 48 | 49 | impl FromRequest for SwapData { 50 | type Error = Error; 51 | type Future = Ready>; 52 | 53 | fn from_request(req: &HttpRequest, _pl: &mut dev::Payload) -> Self::Future { 54 | if let Some(data) = req.app_data::>() { 55 | ready(Ok(SwapData { 56 | swap: Arc::clone(&data.swap), 57 | })) 58 | } else { 59 | debug!( 60 | "Failed to extract `SwapData<{}>` for `{}` handler. For the Data extractor to work \ 61 | correctly, wrap the data with `SwapData::new()` and pass it to `App::app_data()`. \ 62 | Ensure that types align in both the set and retrieve calls.", 63 | core::any::type_name::(), 64 | req.match_name().unwrap_or_else(|| req.path()) 65 | ); 66 | 67 | ready(Err(error::ErrorInternalServerError( 68 | "Requested application data is not configured correctly. \ 69 | View/enable debug logs for more details.", 70 | ))) 71 | } 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use actix_web::test::TestRequest; 78 | 79 | use super::*; 80 | 81 | #[derive(Debug, Clone, PartialEq, Eq)] 82 | struct NonCopy(u32); 83 | 84 | #[actix_web::test] 85 | async fn deref() { 86 | let data = SwapData::new(NonCopy(42)); 87 | let inner_data = data.load(); 88 | let _inner_data: &NonCopy = &inner_data; 89 | } 90 | 91 | #[actix_web::test] 92 | async fn extract_success() { 93 | let data = SwapData::new(NonCopy(42)); 94 | 95 | let req = TestRequest::default().app_data(data).to_http_request(); 96 | let extracted_data = SwapData::::extract(&req).await.unwrap(); 97 | 98 | assert_eq!(**extracted_data.load(), NonCopy(42)); 99 | } 100 | 101 | #[actix_web::test] 102 | async fn extract_fail() { 103 | let req = TestRequest::default().to_http_request(); 104 | SwapData::<()>::extract(&req).await.unwrap_err(); 105 | } 106 | 107 | #[actix_web::test] 108 | async fn store_and_reload() { 109 | let data = SwapData::new(NonCopy(42)); 110 | let initial_data = Guard::into_inner(data.load()); 111 | 112 | let req = TestRequest::default().app_data(data).to_http_request(); 113 | 114 | // first load in handler loads initial value 115 | let extracted_data = SwapData::::extract(&req).await.unwrap(); 116 | assert_eq!(**extracted_data.load(), NonCopy(42)); 117 | 118 | // change data 119 | extracted_data.store(NonCopy(80)); 120 | 121 | // next load in handler loads new value 122 | let extracted_data = SwapData::::extract(&req).await.unwrap(); 123 | assert_eq!(**extracted_data.load(), NonCopy(80)); 124 | 125 | // initial extracted data stays the same 126 | assert_eq!(*initial_data, NonCopy(42)); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /actix-web-lab/examples/body_async_write.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates using an `AsyncWrite` as a response body. 2 | 3 | use std::{io, time::Duration}; 4 | 5 | use actix_web::{ 6 | App, HttpResponse, HttpServer, Responder, get, 7 | http::{ 8 | self, 9 | header::{ContentEncoding, ContentType}, 10 | }, 11 | }; 12 | use actix_web_lab::body; 13 | use async_zip::{ZipEntryBuilder, tokio::write::ZipFileWriter}; 14 | use tokio::{ 15 | fs, 16 | io::{AsyncWrite, AsyncWriteExt as _}, 17 | }; 18 | use tokio_util::compat::TokioAsyncWriteCompatExt as _; 19 | 20 | fn zip_to_io_err(err: async_zip::error::ZipError) -> io::Error { 21 | io::Error::other(err) 22 | } 23 | 24 | async fn read_dir(zipper: &mut ZipFileWriter) -> io::Result<()> 25 | where 26 | W: AsyncWrite + Unpin, 27 | { 28 | let mut path = fs::canonicalize(env!("CARGO_MANIFEST_DIR")).await?; 29 | path.push("examples"); 30 | path.push("assets"); 31 | 32 | tracing::info!("zipping {}", path.display()); 33 | 34 | let mut dir = fs::read_dir(path).await?; 35 | 36 | while let Ok(Some(entry)) = dir.next_entry().await { 37 | if !entry.metadata().await.map(|m| m.is_file()).unwrap_or(false) { 38 | continue; 39 | } 40 | 41 | let mut file = match tokio::fs::OpenOptions::new() 42 | .read(true) 43 | .open(entry.path()) 44 | .await 45 | { 46 | Ok(file) => file.compat_write(), 47 | Err(_) => continue, // we can't read the file 48 | }; 49 | 50 | let filename = match entry.file_name().into_string() { 51 | Ok(filename) => filename, 52 | Err(_) => continue, // the file has a non UTF-8 name 53 | }; 54 | 55 | let mut entry = zipper 56 | .write_entry_stream(ZipEntryBuilder::new( 57 | filename.into(), 58 | async_zip::Compression::Deflate, 59 | )) 60 | .await 61 | .map_err(zip_to_io_err)?; 62 | 63 | futures_util::io::copy(&mut file, &mut entry).await?; 64 | entry.close().await.map_err(zip_to_io_err)?; 65 | } 66 | 67 | Ok(()) 68 | } 69 | 70 | #[get("/")] 71 | async fn index() -> impl Responder { 72 | let (wrt, body) = body::writer(); 73 | 74 | // allow response to be started while this is processing 75 | #[allow(clippy::let_underscore_future)] 76 | let _ = actix_web::rt::spawn(async move { 77 | let mut zipper = ZipFileWriter::new(wrt.compat_write()); 78 | 79 | if let Err(err) = read_dir(&mut zipper).await { 80 | tracing::warn!("Failed to write files from directory to zip: {err}") 81 | } 82 | 83 | if let Err(err) = zipper.close().await { 84 | tracing::warn!("Failed to close zipper: {err}") 85 | } 86 | }); 87 | 88 | HttpResponse::Ok() 89 | .append_header(( 90 | http::header::CONTENT_DISPOSITION, 91 | r#"attachment; filename="folder.zip""#, 92 | )) 93 | .append_header(ContentEncoding::Identity) 94 | .append_header((http::header::CONTENT_TYPE, "application/zip")) 95 | .body(body) 96 | } 97 | 98 | #[get("/plain")] 99 | async fn plaintext() -> impl Responder { 100 | let (mut wrt, body) = body::writer(); 101 | 102 | // allow response to be started while this is processing 103 | #[allow(clippy::let_underscore_future)] 104 | let _ = tokio::spawn(async move { 105 | wrt.write_all(b"saying hello in\n").await?; 106 | 107 | wrt.write_all(b"3\n").await?; 108 | tokio::time::sleep(Duration::from_secs(1)).await; 109 | 110 | wrt.write_all(b"2\n").await?; 111 | tokio::time::sleep(Duration::from_secs(1)).await; 112 | 113 | wrt.write_all(b"1\n").await?; 114 | tokio::time::sleep(Duration::from_secs(1)).await; 115 | 116 | wrt.write_all(b"hello world\n").await 117 | }); 118 | 119 | HttpResponse::Ok() 120 | .append_header(ContentType::plaintext()) 121 | .body(body) 122 | } 123 | 124 | #[actix_web::main] 125 | async fn main() -> io::Result<()> { 126 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 127 | 128 | tracing::info!("staring server at http://localhost:8080"); 129 | 130 | HttpServer::new(|| App::new().service(index).service(plaintext)) 131 | .workers(2) 132 | .bind(("127.0.0.1", 8080))? 133 | .run() 134 | .await 135 | } 136 | -------------------------------------------------------------------------------- /actix-web-lab/examples/json.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates use of alternative JSON extractor with const-generic size limits. 2 | 3 | use actix_web::{ 4 | App, HttpRequest, HttpResponse, HttpServer, Responder, 5 | error::InternalError, 6 | middleware::{Logger, NormalizePath}, 7 | web, 8 | }; 9 | use actix_web_lab::extract::{Json, JsonPayloadError}; 10 | use serde::{Deserialize, Serialize}; 11 | use serde_json::json; 12 | 13 | #[derive(Debug, Serialize, Deserialize)] 14 | struct MyObj { 15 | name: String, 16 | number: i32, 17 | } 18 | 19 | /// This handler uses the JSON extractor with the default size limit. 20 | async fn index( 21 | res: Result, JsonPayloadError>, 22 | req: HttpRequest, 23 | ) -> actix_web::Result { 24 | let item = res.map_err(|err| json_error_handler(err, &req))?; 25 | tracing::debug!("model: {item:?}"); 26 | 27 | Ok(HttpResponse::Ok().json(item.0)) 28 | } 29 | 30 | /// This handler uses the JSON extractor with the default size limit. 31 | async fn json_error( 32 | res: Result, JsonPayloadError>, 33 | ) -> actix_web::Result { 34 | let item = res.map_err(|err| { 35 | tracing::error!("failed to deserialize JSON: {err}"); 36 | let res = HttpResponse::BadGateway().json(json!({ 37 | "error": "invalid_json", 38 | "detail": err.to_string(), 39 | })); 40 | InternalError::from_response(err, res) 41 | })?; 42 | tracing::debug!("model: {item:?}"); 43 | 44 | Ok(HttpResponse::Ok().json(item.0)) 45 | } 46 | 47 | fn json_error_handler(err: JsonPayloadError, _req: &HttpRequest) -> actix_web::Error { 48 | tracing::error!(%err); 49 | 50 | let detail = err.to_string(); 51 | let res = match &err { 52 | JsonPayloadError::ContentType => HttpResponse::UnsupportedMediaType().body(detail), 53 | JsonPayloadError::Deserialize { source: err, .. } if err.source().is_data() => { 54 | HttpResponse::UnprocessableEntity().body(detail) 55 | } 56 | _ => HttpResponse::BadRequest().body(detail), 57 | }; 58 | 59 | InternalError::from_response(err, res).into() 60 | } 61 | 62 | /// This handler uses the JSON extractor with a 1KiB size limit. 63 | async fn extract_item(item: Json, req: HttpRequest) -> HttpResponse { 64 | tracing::info!("model: {item:?}, req: {req:?}"); 65 | HttpResponse::Ok().json(item.0) 66 | } 67 | 68 | /// This handler manually loads the request payload and parses the JSON data. 69 | async fn index_manual(body: web::Bytes) -> actix_web::Result { 70 | // body is loaded, now we can deserialize using serde_json 71 | let obj = serde_json::from_slice::(&body)?; 72 | 73 | Ok(HttpResponse::Ok().json(obj)) 74 | } 75 | 76 | #[actix_web::main] 77 | async fn main() -> std::io::Result<()> { 78 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 79 | 80 | tracing::info!("starting HTTP server at http://localhost:8080"); 81 | 82 | HttpServer::new(|| { 83 | App::new() 84 | .service(web::resource("/extractor").route(web::post().to(index))) 85 | .service(web::resource("/extractor2").route(web::post().to(extract_item))) 86 | .service(web::resource("/extractor3").route(web::post().to(json_error))) 87 | .service(web::resource("/manual").route(web::post().to(index_manual))) 88 | .service(web::resource("/").route(web::post().to(index))) 89 | .wrap(NormalizePath::trim()) 90 | .wrap(Logger::default()) 91 | }) 92 | .bind(("127.0.0.1", 8080))? 93 | .run() 94 | .await 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use actix_web::{App, body::to_bytes, dev::Service, http, test, web}; 100 | 101 | use super::*; 102 | 103 | #[actix_web::test] 104 | async fn test_index() { 105 | let app = 106 | test::init_service(App::new().service(web::resource("/").route(web::post().to(index)))) 107 | .await; 108 | 109 | let req = test::TestRequest::post() 110 | .uri("/") 111 | .set_json(MyObj { 112 | name: "my-name".to_owned(), 113 | number: 43, 114 | }) 115 | .to_request(); 116 | let resp = app.call(req).await.unwrap(); 117 | 118 | assert_eq!(resp.status(), http::StatusCode::OK); 119 | 120 | let body_bytes = to_bytes(resp.into_body()).await.unwrap(); 121 | assert_eq!(body_bytes, r#"{"name":"my-name","number":43}"#); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /actix-web-lab/src/ndjson.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, error::Error as StdError, io::Write as _, sync::LazyLock}; 2 | 3 | use actix_web::{ 4 | HttpResponse, Responder, 5 | body::{BodyStream, MessageBody}, 6 | }; 7 | use bytes::{Bytes, BytesMut}; 8 | use futures_core::Stream; 9 | use futures_util::TryStreamExt as _; 10 | use mime::Mime; 11 | use pin_project_lite::pin_project; 12 | use serde::Serialize; 13 | 14 | use crate::util::{InfallibleStream, MutWriter}; 15 | 16 | static NDJSON_MIME: LazyLock = LazyLock::new(|| "application/x-ndjson".parse().unwrap()); 17 | 18 | pin_project! { 19 | /// A buffered [NDJSON] serializing body stream. 20 | /// 21 | /// This has significant memory efficiency advantages over returning an array of JSON objects 22 | /// when the data set is very large because it avoids buffering the entire response. 23 | /// 24 | /// # Examples 25 | /// ``` 26 | /// # use actix_web::Responder; 27 | /// # use actix_web_lab::respond::NdJson; 28 | /// # use futures_core::Stream; 29 | /// fn streaming_data_source() -> impl Stream { 30 | /// // get item stream from source 31 | /// # futures_util::stream::empty() 32 | /// } 33 | /// 34 | /// async fn handler() -> impl Responder { 35 | /// let data_stream = streaming_data_source(); 36 | /// 37 | /// NdJson::new_infallible(data_stream) 38 | /// .into_responder() 39 | /// } 40 | /// ``` 41 | /// 42 | /// [NDJSON]: https://github.com/ndjson/ndjson-spec 43 | pub struct NdJson { 44 | // The wrapped item stream. 45 | #[pin] 46 | stream: S, 47 | } 48 | } 49 | 50 | impl NdJson { 51 | /// Constructs a new `NdJson` from a stream of items. 52 | pub fn new(stream: S) -> Self { 53 | Self { stream } 54 | } 55 | } 56 | 57 | impl NdJson { 58 | /// Constructs a new `NdJson` from an infallible stream of items. 59 | pub fn new_infallible(stream: S) -> NdJson> { 60 | NdJson::new(InfallibleStream::new(stream)) 61 | } 62 | } 63 | 64 | impl NdJson 65 | where 66 | S: Stream>, 67 | T: Serialize, 68 | E: Into> + 'static, 69 | { 70 | /// Creates a chunked body stream that serializes as NDJSON on-the-fly. 71 | pub fn into_body_stream(self) -> impl MessageBody { 72 | BodyStream::new(self.into_chunk_stream()) 73 | } 74 | 75 | /// Creates a `Responder` type with a serializing stream and correct Content-Type header. 76 | pub fn into_responder(self) -> impl Responder 77 | where 78 | S: 'static, 79 | T: 'static, 80 | E: 'static, 81 | { 82 | HttpResponse::Ok() 83 | .content_type(NDJSON_MIME.clone()) 84 | .message_body(self.into_body_stream()) 85 | .unwrap() 86 | } 87 | 88 | /// Creates a stream of serialized chunks. 89 | pub fn into_chunk_stream(self) -> impl Stream> { 90 | self.stream.map_ok(serialize_json_line) 91 | } 92 | } 93 | 94 | impl NdJson { 95 | /// Returns the NDJSON MIME type (`application/x-ndjson`). 96 | pub fn mime() -> Mime { 97 | NDJSON_MIME.clone() 98 | } 99 | } 100 | 101 | fn serialize_json_line(item: impl Serialize) -> Bytes { 102 | let mut buf = BytesMut::new(); 103 | let mut wrt = MutWriter(&mut buf); 104 | 105 | // serialize JSON line to buffer 106 | serde_json::to_writer(&mut wrt, &item).unwrap(); 107 | 108 | // add line break to buffer 109 | wrt.write_all(b"\n").unwrap(); 110 | 111 | buf.freeze() 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use std::error::Error as StdError; 117 | 118 | use actix_web::body; 119 | use futures_util::stream; 120 | use serde_json::json; 121 | 122 | use super::*; 123 | 124 | #[actix_web::test] 125 | async fn serializes_into_body() { 126 | let ndjson_body = NdJson::new_infallible(stream::iter(vec![ 127 | json!(null), 128 | json!(1u32), 129 | json!("123"), 130 | json!({ "abc": "123" }), 131 | json!(["abc", 123u32]), 132 | ])) 133 | .into_body_stream(); 134 | 135 | let body_bytes = body::to_bytes(ndjson_body) 136 | .await 137 | .map_err(Into::>::into) 138 | .unwrap(); 139 | 140 | const EXP_BYTES: &str = "null\n\ 141 | 1\n\ 142 | \"123\"\n\ 143 | {\"abc\":\"123\"}\n\ 144 | [\"abc\",123]\n"; 145 | 146 | assert_eq!(body_bytes, EXP_BYTES); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /actix-web-lab/src/util.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for working with Actix Web types. 2 | 3 | // stuff in here comes in and out of usage 4 | #![allow(dead_code)] 5 | 6 | use std::{ 7 | convert::Infallible, 8 | io, 9 | pin::Pin, 10 | task::{Context, Poll, ready}, 11 | }; 12 | 13 | use actix_http::{BoxedPayloadStream, error::PayloadError}; 14 | use actix_web::{dev, web::BufMut}; 15 | use futures_core::Stream; 16 | use futures_util::StreamExt as _; 17 | use local_channel::mpsc; 18 | 19 | /// Returns an effectively cloned payload that supports streaming efficiently. 20 | /// 21 | /// The cloned payload: 22 | /// - yields identical chunks; 23 | /// - does not poll ahead of the original; 24 | /// - does not poll significantly slower than the original; 25 | /// - receives an error signal if the original errors, but details are opaque to the copy. 26 | /// 27 | /// If the payload is forked in one of the extractors used in a handler, then the original _must_ be 28 | /// read in another extractor or else the request will hang. 29 | pub fn fork_request_payload(orig_payload: &mut dev::Payload) -> dev::Payload { 30 | const TARGET: &str = concat!(module_path!(), "::fork_request_payload"); 31 | 32 | let payload = orig_payload.take(); 33 | 34 | let (tx, rx) = mpsc::channel(); 35 | 36 | let proxy_stream: BoxedPayloadStream = Box::pin(payload.inspect(move |res| { 37 | match res { 38 | Ok(chunk) => { 39 | tracing::trace!(target: TARGET, "yielding {} byte chunk", chunk.len()); 40 | tx.send(Ok(chunk.clone())).unwrap(); 41 | } 42 | 43 | Err(err) => tx 44 | .send(Err(PayloadError::Io(io::Error::other(format!( 45 | "error from original stream: {err}" 46 | ))))) 47 | .unwrap(), 48 | } 49 | })); 50 | 51 | tracing::trace!(target: TARGET, "creating proxy payload"); 52 | *orig_payload = dev::Payload::from(proxy_stream); 53 | 54 | dev::Payload::Stream { 55 | payload: Box::pin(rx), 56 | } 57 | } 58 | 59 | /// An `io::Write`r that only requires mutable reference and assumes that there is space available 60 | /// in the buffer for every write operation or that it can be extended implicitly (like 61 | /// `bytes::BytesMut`, for example). 62 | /// 63 | /// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not 64 | /// perform a remaining length check before writing. 65 | pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B); 66 | 67 | impl MutWriter<'_, B> { 68 | pub fn get_ref(&self) -> &B { 69 | self.0 70 | } 71 | } 72 | 73 | impl io::Write for MutWriter<'_, B> { 74 | fn write(&mut self, buf: &[u8]) -> io::Result { 75 | self.0.put_slice(buf); 76 | Ok(buf.len()) 77 | } 78 | 79 | fn flush(&mut self) -> io::Result<()> { 80 | Ok(()) 81 | } 82 | } 83 | 84 | pin_project_lite::pin_project! { 85 | /// Converts stream with item `T` into `Result`. 86 | pub struct InfallibleStream { 87 | #[pin] 88 | stream: S, 89 | } 90 | } 91 | 92 | impl InfallibleStream { 93 | /// Constructs new `InfallibleStream` stream. 94 | pub fn new(stream: S) -> Self { 95 | Self { stream } 96 | } 97 | } 98 | 99 | impl Stream for InfallibleStream { 100 | type Item = Result; 101 | 102 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 103 | Poll::Ready(ready!(self.project().stream.poll_next(cx)).map(Ok)) 104 | } 105 | 106 | fn size_hint(&self) -> (usize, Option) { 107 | self.stream.size_hint() 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | #[derive(Debug, Clone, Default)] 113 | pub(crate) struct PollSeq { 114 | seq: std::collections::VecDeque, 115 | } 116 | 117 | #[cfg(test)] 118 | mod poll_seq_impls { 119 | use std::collections::VecDeque; 120 | 121 | use futures_util::stream; 122 | 123 | use super::*; 124 | 125 | impl PollSeq { 126 | pub fn new(seq: VecDeque) -> Self { 127 | Self { seq } 128 | } 129 | } 130 | 131 | impl PollSeq>> { 132 | pub fn into_stream(mut self) -> impl Stream { 133 | stream::poll_fn(move |_cx| match self.seq.pop_front() { 134 | Some(item) => item, 135 | None => Poll::Ready(None), 136 | }) 137 | } 138 | } 139 | 140 | impl From<[T; N]> for PollSeq { 141 | fn from(seq: [T; N]) -> Self { 142 | Self::new(VecDeque::from(seq)) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /actix-web-lab/examples/req_sig.rs: -------------------------------------------------------------------------------- 1 | //! Implements `RequestSignatureScheme` for a made-up API. 2 | 3 | use std::io; 4 | 5 | use actix_web::{ 6 | App, Error, HttpRequest, HttpServer, error, 7 | http::header::HeaderValue, 8 | middleware::Logger, 9 | web::{self, Bytes}, 10 | }; 11 | use actix_web_lab::extract::{RequestSignature, RequestSignatureScheme}; 12 | use base64::prelude::*; 13 | use digest::{CtOutput, Digest, Mac}; 14 | use generic_array::GenericArray; 15 | use hmac::SimpleHmac; 16 | use sha2::{Sha256, Sha512}; 17 | use tracing::info; 18 | 19 | #[allow(non_upper_case_globals)] 20 | const db: () = (); 21 | 22 | async fn lookup_public_key_in_db(_db: &(), val: T) -> T { 23 | val 24 | } 25 | 26 | /// Extracts user's public key from request and pretends to look up secret key in the DB. 27 | async fn get_base64_api_key(req: &HttpRequest) -> actix_web::Result> { 28 | // public key, not encryption key 29 | let pub_key = req 30 | .headers() 31 | .get("Api-Key") 32 | .map(HeaderValue::as_bytes) 33 | .map(|bytes| BASE64_STANDARD.decode(bytes)) 34 | .transpose() 35 | .map_err(|_| error::ErrorInternalServerError("invalid api key"))? 36 | .ok_or_else(|| error::ErrorUnauthorized("api key not provided"))?; 37 | 38 | // in a real app it would be something like: 39 | // let db = req.app_data::>().unwrap(); 40 | let secret_key = lookup_public_key_in_db(&db, pub_key).await; 41 | 42 | Ok(secret_key) 43 | } 44 | 45 | fn get_user_signature(req: &HttpRequest) -> actix_web::Result> { 46 | req.headers() 47 | .get("Signature") 48 | .map(HeaderValue::as_bytes) 49 | .map(|bytes| BASE64_STANDARD.decode(bytes)) 50 | .transpose() 51 | .map_err(|_| error::ErrorInternalServerError("invalid signature"))? 52 | .ok_or_else(|| error::ErrorUnauthorized("signature not provided")) 53 | } 54 | 55 | #[derive(Debug, Default)] 56 | struct ExampleApi { 57 | /// Key derived from fetching user's API private key from database. 58 | key: Vec, 59 | 60 | /// Payload hash state. 61 | hasher: Sha256, 62 | } 63 | 64 | impl RequestSignatureScheme for ExampleApi { 65 | type Signature = CtOutput>; 66 | type Error = Error; 67 | 68 | async fn init(req: &HttpRequest) -> Result { 69 | let key = get_base64_api_key(req).await?; 70 | 71 | let mut hasher = Sha256::new(); 72 | 73 | // optional nonce 74 | if let Some(nonce) = req.headers().get("nonce") { 75 | Digest::update(&mut hasher, nonce.as_bytes()); 76 | } 77 | 78 | if let Some(path) = req.uri().path_and_query() { 79 | Digest::update(&mut hasher, path.as_str().as_bytes()) 80 | } 81 | 82 | Ok(Self { key, hasher }) 83 | } 84 | 85 | async fn consume_chunk(&mut self, _req: &HttpRequest, chunk: Bytes) -> Result<(), Self::Error> { 86 | Digest::update(&mut self.hasher, &chunk); 87 | Ok(()) 88 | } 89 | 90 | async fn finalize(self, _req: &HttpRequest) -> Result { 91 | println!("using key: {:X?}", &self.key); 92 | 93 | let mut hmac = >::new_from_slice(&self.key).unwrap(); 94 | 95 | let payload_hash = self.hasher.finalize(); 96 | println!("payload hash: {payload_hash:X?}"); 97 | Mac::update(&mut hmac, &payload_hash); 98 | 99 | Ok(hmac.finalize()) 100 | } 101 | 102 | fn verify( 103 | signature: Self::Signature, 104 | req: &HttpRequest, 105 | ) -> Result { 106 | let user_sig = get_user_signature(req)?; 107 | let user_sig = CtOutput::new(GenericArray::from_slice(&user_sig).to_owned()); 108 | 109 | if signature == user_sig { 110 | Ok(signature) 111 | } else { 112 | Err(error::ErrorUnauthorized( 113 | "given signature does not match calculated signature", 114 | )) 115 | } 116 | } 117 | } 118 | 119 | #[actix_web::main] 120 | async fn main() -> io::Result<()> { 121 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 122 | 123 | info!("staring server at http://localhost:8080"); 124 | 125 | HttpServer::new(|| { 126 | App::new().wrap(Logger::default().log_target("@")).route( 127 | "/", 128 | web::post().to(|body: RequestSignature| async move { 129 | let (body, sig) = body.into_parts(); 130 | let sig = sig.into_bytes().to_vec(); 131 | format!("{body:?}\n\n{sig:x?}") 132 | }), 133 | ) 134 | }) 135 | .workers(1) 136 | .bind(("127.0.0.1", 8080))? 137 | .run() 138 | .await 139 | } 140 | -------------------------------------------------------------------------------- /actix-web-lab/src/spa.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use actix_files::{Files, NamedFile}; 4 | use actix_service::fn_service; 5 | use actix_web::dev::{HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}; 6 | use tracing::trace; 7 | 8 | /// Single Page App (SPA) service builder. 9 | /// 10 | /// # Examples 11 | /// 12 | /// ```no_run 13 | /// # use actix_web::App; 14 | /// # use actix_web_lab::web::spa; 15 | /// App::new() 16 | /// // ...api routes... 17 | /// .service( 18 | /// spa() 19 | /// .index_file("./examples/assets/spa.html") 20 | /// .static_resources_mount("/static") 21 | /// .static_resources_location("./examples/assets") 22 | /// .finish(), 23 | /// ) 24 | /// # ; 25 | /// ``` 26 | #[derive(Debug, Clone)] 27 | pub struct Spa { 28 | index_file: Cow<'static, str>, 29 | static_resources_mount: Cow<'static, str>, 30 | static_resources_location: Cow<'static, str>, 31 | } 32 | 33 | impl Spa { 34 | /// Location of the SPA index file. 35 | /// 36 | /// This file will be served if: 37 | /// - the Actix Web router has reached this service, indicating that none of the API routes 38 | /// matched the URL path; 39 | /// - and none of the static resources handled matched. 40 | /// 41 | /// The default is "./index.html". I.e., the `index.html` file located in the directory that 42 | /// the server is running from. 43 | pub fn index_file(mut self, index_file: impl Into>) -> Self { 44 | self.index_file = index_file.into(); 45 | self 46 | } 47 | 48 | /// The URL path prefix that static files should be served from. 49 | /// 50 | /// The default is "/". I.e., static files are served from the root URL path. 51 | pub fn static_resources_mount( 52 | mut self, 53 | static_resources_mount: impl Into>, 54 | ) -> Self { 55 | self.static_resources_mount = static_resources_mount.into(); 56 | self 57 | } 58 | 59 | /// The location in the filesystem to serve static resources from. 60 | /// 61 | /// The default is "./". I.e., static files are located in the directory the server is 62 | /// running from. 63 | pub fn static_resources_location( 64 | mut self, 65 | static_resources_location: impl Into>, 66 | ) -> Self { 67 | self.static_resources_location = static_resources_location.into(); 68 | self 69 | } 70 | 71 | /// Constructs the service for use in a `.service()` call. 72 | pub fn finish(self) -> impl HttpServiceFactory { 73 | let index_file = self.index_file.into_owned(); 74 | let static_resources_location = self.static_resources_location.into_owned(); 75 | let static_resources_mount = self.static_resources_mount.into_owned(); 76 | 77 | let files = { 78 | let index_file = index_file.clone(); 79 | Files::new(&static_resources_mount, static_resources_location) 80 | // HACK: FilesService will try to read a directory listing unless index_file is provided 81 | // FilesService will fail to load the index_file and will then call our default_handler 82 | .index_file("extremely-unlikely-to-exist-!@$%^&*.txt") 83 | .default_handler(move |req| serve_index(req, index_file.clone())) 84 | }; 85 | 86 | SpaService { index_file, files } 87 | } 88 | } 89 | 90 | #[derive(Debug)] 91 | struct SpaService { 92 | index_file: String, 93 | files: Files, 94 | } 95 | 96 | impl HttpServiceFactory for SpaService { 97 | fn register(self, config: &mut actix_web::dev::AppService) { 98 | // let Files register its mount path as-is 99 | self.files.register(config); 100 | 101 | // also define a root prefix handler directed towards our SPA index 102 | let rdef = ResourceDef::root_prefix(""); 103 | config.register_service( 104 | rdef, 105 | None, 106 | fn_service(move |req| serve_index(req, self.index_file.clone())), 107 | None, 108 | ); 109 | } 110 | } 111 | 112 | async fn serve_index( 113 | req: ServiceRequest, 114 | index_file: String, 115 | ) -> Result { 116 | trace!("serving default SPA page"); 117 | let (req, _) = req.into_parts(); 118 | let file = NamedFile::open_async(&index_file).await?; 119 | let res = file.into_response(&req); 120 | Ok(ServiceResponse::new(req, res)) 121 | } 122 | 123 | impl Default for Spa { 124 | fn default() -> Self { 125 | Self { 126 | index_file: Cow::Borrowed("./index.html"), 127 | static_resources_mount: Cow::Borrowed("/"), 128 | static_resources_location: Cow::Borrowed("./"), 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | _list: 2 | @just --list 3 | 4 | # Check project formatting. 5 | check: && clippy 6 | just --unstable --fmt --check 7 | fd --hidden --type=file -e=md -e=yml --exec-batch prettier --check 8 | fd --hidden -e=toml --exec-batch taplo format --check 9 | fd --hidden -e=toml --exec-batch taplo lint 10 | cargo +nightly fmt -- --check 11 | 12 | # Format project. 13 | [group("lint")] 14 | fmt: update-readmes 15 | just --unstable --fmt 16 | nixpkgs-fmt . 17 | fd --hidden --type=file -e=md -e=yml --exec-batch prettier --write 18 | fd --type=file --hidden -e=toml --exec-batch taplo format 19 | cargo +nightly fmt 20 | 21 | # Update READMEs from crate root documentation. 22 | [group("lint")] 23 | update-readmes: 24 | cd ./russe && cargo rdme --force 25 | cd ./err-report && cargo rdme --force 26 | 27 | msrv := ``` 28 | cargo metadata --format-version=1 \ 29 | | jq -r 'first(.packages[] | select(.source == null and .rust_version)) | .rust_version' \ 30 | | sed -E 's/^1\.([0-9]{2})$/1\.\1\.0/' 31 | ``` 32 | msrv_rustup := "+" + msrv 33 | 34 | # Downgrade dev-dependencies necessary to run MSRV checks/tests. 35 | [private] 36 | downgrade-for-msrv: 37 | @ echo "No downgrades currently needed for MSRV testing" 38 | 39 | # Run tests on all crates in workspace using specified (or default) toolchain. 40 | clippy toolchain="": 41 | cargo {{ toolchain }} clippy --workspace --all-targets --all-features 42 | 43 | # Run tests on all crates in workspace using specified (or default) toolchain and watch for changes. 44 | clippy-watch toolchain="": 45 | cargo watch -- just clippy {{ toolchain }} 46 | 47 | # Run tests on all crates in workspace using its MSRV. 48 | test-msrv: downgrade-for-msrv (test msrv_rustup) 49 | 50 | # Run tests on all crates in workspace using specified (or default) toolchain. 51 | test toolchain="": 52 | cargo {{ toolchain }} nextest run --no-default-features 53 | cargo {{ toolchain }} nextest run 54 | cargo {{ toolchain }} nextest run --all-features 55 | 56 | # Run tests on all crates in workspace and produce coverage file (Codecov format). 57 | test-coverage-codecov toolchain="": 58 | cargo {{ toolchain }} llvm-cov --workspace --all-features --codecov --output-path codecov.json 59 | 60 | # Run tests on all crates in workspace and produce coverage file (lcov format). 61 | test-coverage-lcov toolchain="": 62 | cargo {{ toolchain }} llvm-cov --workspace --all-features --lcov --output-path lcov.info 63 | 64 | # Test workspace docs. 65 | test-docs toolchain="": && doc 66 | cargo {{ toolchain }} test --doc --workspace --all-features --no-fail-fast -- --nocapture 67 | 68 | # Document crates in workspace. 69 | doc *args: && doc-set-workspace-crates 70 | rm -f "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js" 71 | RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --no-deps --workspace --all-features {{ args }} 72 | 73 | [private] 74 | doc-set-workspace-crates: 75 | #!/usr/bin/env bash 76 | ( 77 | echo "window.ALL_CRATES =" 78 | cargo metadata --format-version=1 \ 79 | | jq '[.packages[] | select(.source == null) | .targets | map(select(.doc) | .name)] | flatten' 80 | echo ";" 81 | ) > "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js" 82 | 83 | # Build rustdoc for all crates in workspace and watch for changes. 84 | doc-watch: 85 | RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --all-features --open 86 | cargo watch -- RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --all-features 87 | 88 | # Check for unintentional external type exposure on all crates in workspace. 89 | check-external-types-all toolchain="+nightly-2024-05-01": 90 | #!/usr/bin/env bash 91 | set -euo pipefail 92 | exit=0 93 | for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do 94 | if ! just check-external-types-manifest "$f" {{ toolchain }}; then exit=1; fi 95 | echo 96 | echo 97 | done 98 | exit $exit 99 | 100 | # Check for unintentional external type exposure on all crates in workspace. 101 | check-external-types-all-table toolchain="+nightly-2024-05-01": 102 | #!/usr/bin/env bash 103 | set -euo pipefail 104 | for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do 105 | echo 106 | echo "Checking for $f" 107 | just check-external-types-manifest "$f" {{ toolchain }} --output-format=markdown-table 108 | done 109 | 110 | # Check for unintentional external type exposure on a crate. 111 | check-external-types-manifest manifest_path toolchain="+nightly-2024-05-01" *extra_args="": 112 | cargo {{ toolchain }} check-external-types --manifest-path "{{ manifest_path }}" {{ extra_args }} 113 | -------------------------------------------------------------------------------- /actix-proxy-protocol/src/v1/mod.rs: -------------------------------------------------------------------------------- 1 | mod service; 2 | 3 | use std::{fmt, io, net::SocketAddr}; 4 | 5 | use arrayvec::ArrayVec; 6 | use nom::{IResult, Parser as _}; 7 | use tokio::io::{AsyncWrite, AsyncWriteExt as _}; 8 | 9 | pub use self::service::{Acceptor, AcceptorService, TlsError, TlsStream}; 10 | use crate::AddressFamily; 11 | 12 | pub const SIGNATURE: &str = "PROXY"; 13 | pub const MAX_HEADER_SIZE: usize = 107; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct Header { 17 | /// Address family. 18 | af: AddressFamily, 19 | 20 | /// Source address. 21 | src: SocketAddr, 22 | 23 | /// Destination address. 24 | dst: SocketAddr, 25 | } 26 | 27 | impl Header { 28 | pub const fn new(af: AddressFamily, src: SocketAddr, dst: SocketAddr) -> Self { 29 | Self { af, src, dst } 30 | } 31 | 32 | pub const fn new_inet(src: SocketAddr, dst: SocketAddr) -> Self { 33 | Self::new(AddressFamily::Inet, src, dst) 34 | } 35 | 36 | pub const fn new_inet6(src: SocketAddr, dst: SocketAddr) -> Self { 37 | Self::new(AddressFamily::Inet6, src, dst) 38 | } 39 | 40 | pub fn write_to(&self, wrt: &mut impl io::Write) -> io::Result<()> { 41 | write!(wrt, "{self}") 42 | } 43 | 44 | pub async fn write_to_tokio(&self, wrt: &mut (impl AsyncWrite + Unpin)) -> io::Result<()> { 45 | // max length of a V1 header is 107 bytes 46 | let mut buf = ArrayVec::<_, MAX_HEADER_SIZE>::new(); 47 | self.write_to(&mut buf)?; 48 | wrt.write_all(&buf).await 49 | } 50 | 51 | pub fn try_from_bytes(slice: &[u8]) -> IResult<&[u8], Self> { 52 | parsing::parse(slice) 53 | } 54 | } 55 | 56 | impl fmt::Display for Header { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | write!( 59 | f, 60 | "{proto_sig} {af} {src_ip} {dst_ip} {src_port} {dst_port}\r\n", 61 | proto_sig = SIGNATURE, 62 | af = self.af.v1_str(), 63 | src_ip = self.src.ip(), 64 | dst_ip = self.dst.ip(), 65 | src_port = itoa::Buffer::new().format(self.src.port()), 66 | dst_port = itoa::Buffer::new().format(self.dst.port()), 67 | ) 68 | } 69 | } 70 | 71 | mod parsing { 72 | use std::{ 73 | net::{Ipv4Addr, SocketAddrV4}, 74 | str::{self, FromStr}, 75 | }; 76 | 77 | use nom::{ 78 | IResult, 79 | branch::alt, 80 | bytes::complete::{tag, take_while}, 81 | character::complete::char, 82 | combinator::{map, map_res}, 83 | }; 84 | 85 | use super::*; 86 | 87 | /// Parses a number from serialized representation (as bytes). 88 | fn parse_number(input: &[u8]) -> IResult<&[u8], T> { 89 | map_res(take_while(|c: u8| c.is_ascii_digit()), |s: &[u8]| { 90 | let s = str::from_utf8(s).map_err(|_| "utf8 error")?; 91 | let val = s.parse::().map_err(|_| "u8 parse error")?; 92 | Ok::<_, Box>(val) 93 | }) 94 | .parse(input) 95 | } 96 | 97 | /// Parses an address family. 98 | fn parse_address_family(input: &[u8]) -> IResult<&[u8], AddressFamily> { 99 | map_res(alt((tag("TCP4"), tag("TCP6"))), |af: &[u8]| match af { 100 | b"TCP4" => Ok(AddressFamily::Inet), 101 | b"TCP6" => Ok(AddressFamily::Inet6), 102 | _ => Err(io::Error::new( 103 | io::ErrorKind::InvalidData, 104 | "invalid address family", 105 | )), 106 | }) 107 | .parse(input) 108 | } 109 | 110 | /// Parses an IPv4 address from serialized representation (as bytes). 111 | fn parse_ipv4(input: &[u8]) -> IResult<&[u8], Ipv4Addr> { 112 | map( 113 | ( 114 | parse_number::, 115 | char('.'), 116 | parse_number::, 117 | char('.'), 118 | parse_number::, 119 | char('.'), 120 | parse_number::, 121 | ), 122 | |(a, _, b, _, c, _, d)| Ipv4Addr::new(a, b, c, d), 123 | ) 124 | .parse(input) 125 | } 126 | 127 | /// Parses an IPv4 address from ASCII bytes. 128 | pub(super) fn parse(input: &[u8]) -> IResult<&[u8], Header> { 129 | map( 130 | ( 131 | tag(SIGNATURE), 132 | char(' '), 133 | parse_address_family, 134 | char(' '), 135 | parse_ipv4, 136 | char(' '), 137 | parse_ipv4, 138 | char(' '), 139 | parse_number::, 140 | char(' '), 141 | parse_number::, 142 | ), 143 | |(_, _, af, _, src_ip, _, dst_ip, _, src_port, _, dst_port)| Header { 144 | af, 145 | src: SocketAddr::V4(SocketAddrV4::new(src_ip, src_port)), 146 | dst: SocketAddr::V4(SocketAddrV4::new(dst_ip, dst_port)), 147 | }, 148 | ) 149 | .parse(input) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /actix-web-lab/src/test_response_macros.rs: -------------------------------------------------------------------------------- 1 | /// Quickly write tests that check various parts of a `ServiceResponse`. 2 | /// 3 | /// An async test must be used (e.g., `#[actix_web::test]`) if used to assert on response body. 4 | /// 5 | /// # Examples 6 | /// ``` 7 | /// use actix_web::{ 8 | /// dev::ServiceResponse, http::header::ContentType, test::TestRequest, HttpResponse, 9 | /// }; 10 | /// use actix_web_lab::assert_response_matches; 11 | /// 12 | /// # actix_web::rt::System::new().block_on(async { 13 | /// let res = ServiceResponse::new( 14 | /// TestRequest::default().to_http_request(), 15 | /// HttpResponse::Created() 16 | /// .insert_header(("date", "today")) 17 | /// .insert_header(("set-cookie", "a=b")) 18 | /// .body("Hello World!"), 19 | /// ); 20 | /// 21 | /// assert_response_matches!(res, CREATED); 22 | /// assert_response_matches!(res, CREATED; "date" => "today"); 23 | /// assert_response_matches!(res, CREATED; @raw "Hello World!"); 24 | /// 25 | /// let res = ServiceResponse::new( 26 | /// TestRequest::default().to_http_request(), 27 | /// HttpResponse::Created() 28 | /// .insert_header(("date", "today")) 29 | /// .insert_header(("set-cookie", "a=b")) 30 | /// .body("Hello World!"), 31 | /// ); 32 | /// 33 | /// assert_response_matches!(res, CREATED; 34 | /// "date" => "today" 35 | /// "set-cookie" => "a=b"; 36 | /// @raw "Hello World!" 37 | /// ); 38 | /// 39 | /// let res = ServiceResponse::new( 40 | /// TestRequest::default().to_http_request(), 41 | /// HttpResponse::Created() 42 | /// .content_type(ContentType::json()) 43 | /// .insert_header(("date", "today")) 44 | /// .insert_header(("set-cookie", "a=b")) 45 | /// .body(r#"{"abc":"123"}"#), 46 | /// ); 47 | /// 48 | /// assert_response_matches!(res, CREATED; @json { "abc": "123" }); 49 | /// # }); 50 | /// ``` 51 | #[macro_export] 52 | macro_rules! assert_response_matches { 53 | ($res:ident, $status:ident) => {{ 54 | assert_eq!($res.status(), ::actix_web::http::StatusCode::$status) 55 | }}; 56 | 57 | ($res:ident, $status:ident; $($hdr_name:expr => $hdr_val:expr)+) => {{ 58 | assert_response_matches!($res, $status); 59 | 60 | $( 61 | assert_eq!( 62 | $res.headers().get(::actix_web::http::header::HeaderName::from_static($hdr_name)).unwrap(), 63 | &::actix_web::http::header::HeaderValue::from_static($hdr_val), 64 | ); 65 | )+ 66 | }}; 67 | 68 | ($res:ident, $status:ident; @raw $payload:expr) => {{ 69 | assert_response_matches!($res, $status); 70 | assert_eq!(::actix_web::test::read_body($res).await, $payload); 71 | }}; 72 | 73 | ($res:ident, $status:ident; $($hdr_name:expr => $hdr_val:expr)+; @raw $payload:expr) => {{ 74 | assert_response_matches!($res, $status; $($hdr_name => $hdr_val)+); 75 | assert_eq!(::actix_web::test::read_body($res).await, $payload); 76 | }}; 77 | 78 | ($res:ident, $status:ident; @json $payload:tt) => {{ 79 | assert_response_matches!($res, $status); 80 | assert_eq!( 81 | ::actix_web::test::read_body_json::<$crate::__reexports::serde_json::Value, _>($res).await, 82 | $crate::__reexports::serde_json::json!($payload), 83 | ); 84 | }}; 85 | } 86 | 87 | pub use assert_response_matches; 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use actix_web::{ 92 | HttpResponse, dev::ServiceResponse, http::header::ContentType, test::TestRequest, 93 | }; 94 | 95 | use super::*; 96 | 97 | #[actix_web::test] 98 | async fn response_matching() { 99 | let res = ServiceResponse::new( 100 | TestRequest::default().to_http_request(), 101 | HttpResponse::Created() 102 | .insert_header(("date", "today")) 103 | .insert_header(("set-cookie", "a=b")) 104 | .body("Hello World!"), 105 | ); 106 | 107 | assert_response_matches!(res, CREATED); 108 | assert_response_matches!(res, CREATED; "date" => "today"); 109 | assert_response_matches!(res, CREATED; @raw "Hello World!"); 110 | 111 | let res = ServiceResponse::new( 112 | TestRequest::default().to_http_request(), 113 | HttpResponse::Created() 114 | .insert_header(("date", "today")) 115 | .insert_header(("set-cookie", "a=b")) 116 | .body("Hello World!"), 117 | ); 118 | assert_response_matches!(res, CREATED; 119 | "date" => "today" 120 | "set-cookie" => "a=b"; 121 | @raw "Hello World!" 122 | ); 123 | 124 | let res = ServiceResponse::new( 125 | TestRequest::default().to_http_request(), 126 | HttpResponse::Created() 127 | .content_type(ContentType::json()) 128 | .insert_header(("date", "today")) 129 | .insert_header(("set-cookie", "a=b")) 130 | .body(r#"{"abc":"123"}"#), 131 | ); 132 | 133 | assert_response_matches!(res, CREATED; @json { "abc": "123" }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /actix-web-lab/src/panic_reporter.rs: -------------------------------------------------------------------------------- 1 | //! Panic reporter middleware. 2 | //! 3 | //! See [`PanicReporter`] for docs. 4 | 5 | use std::{ 6 | any::Any, 7 | future::{Ready, ready}, 8 | panic::{self, AssertUnwindSafe}, 9 | rc::Rc, 10 | }; 11 | 12 | use actix_web::dev::{Service, Transform, forward_ready}; 13 | use futures_core::future::LocalBoxFuture; 14 | use futures_util::FutureExt as _; 15 | 16 | type PanicCallback = Rc; 17 | 18 | /// A middleware that triggers a callback when the worker is panicking. 19 | /// 20 | /// Mostly useful for logging or metrics publishing. The callback received the object with which 21 | /// panic was originally invoked to allow down-casting. 22 | /// 23 | /// # Examples 24 | /// 25 | /// ```no_run 26 | /// # use actix_web::App; 27 | /// use actix_web_lab::middleware::PanicReporter; 28 | /// # mod metrics { 29 | /// # macro_rules! increment_counter { 30 | /// # ($tt:tt) => {{}}; 31 | /// # } 32 | /// # pub(crate) use increment_counter; 33 | /// # } 34 | /// 35 | /// App::new().wrap(PanicReporter::new(|_| metrics::increment_counter!("panic"))) 36 | /// # ; 37 | /// ``` 38 | #[derive(Clone)] 39 | pub struct PanicReporter { 40 | cb: PanicCallback, 41 | } 42 | 43 | impl PanicReporter { 44 | /// Constructs new panic reporter middleware with `callback`. 45 | pub fn new(callback: impl Fn(&(dyn Any + Send)) + 'static) -> Self { 46 | Self { 47 | cb: Rc::new(callback), 48 | } 49 | } 50 | } 51 | 52 | impl std::fmt::Debug for PanicReporter { 53 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 54 | f.debug_struct("PanicReporter") 55 | .field("cb", &"") 56 | .finish() 57 | } 58 | } 59 | 60 | impl Transform for PanicReporter 61 | where 62 | S: Service, 63 | S::Future: 'static, 64 | { 65 | type Response = S::Response; 66 | type Error = S::Error; 67 | type Transform = PanicReporterMiddleware; 68 | type InitError = (); 69 | type Future = Ready>; 70 | 71 | fn new_transform(&self, service: S) -> Self::Future { 72 | ready(Ok(PanicReporterMiddleware { 73 | service: Rc::new(service), 74 | cb: Rc::clone(&self.cb), 75 | })) 76 | } 77 | } 78 | 79 | /// Middleware service implementation for [`PanicReporter`]. 80 | #[doc(hidden)] 81 | #[allow(missing_debug_implementations)] 82 | pub struct PanicReporterMiddleware { 83 | service: Rc, 84 | cb: PanicCallback, 85 | } 86 | 87 | impl Service for PanicReporterMiddleware 88 | where 89 | S: Service, 90 | S::Future: 'static, 91 | { 92 | type Response = S::Response; 93 | type Error = S::Error; 94 | type Future = LocalBoxFuture<'static, Result>; 95 | 96 | forward_ready!(service); 97 | 98 | fn call(&self, req: Req) -> Self::Future { 99 | let cb = Rc::clone(&self.cb); 100 | 101 | // catch panics in service call 102 | AssertUnwindSafe(self.service.call(req)) 103 | .catch_unwind() 104 | .map(move |maybe_res| match maybe_res { 105 | Ok(res) => res, 106 | Err(panic_err) => { 107 | // invoke callback with panic arg 108 | (cb)(&panic_err); 109 | 110 | // continue unwinding 111 | panic::resume_unwind(panic_err) 112 | } 113 | }) 114 | .boxed_local() 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use std::sync::{ 121 | Arc, 122 | atomic::{AtomicBool, Ordering}, 123 | }; 124 | 125 | use actix_web::{ 126 | App, 127 | dev::Service as _, 128 | test, 129 | web::{self, ServiceConfig}, 130 | }; 131 | 132 | use super::*; 133 | 134 | fn configure_test_app(cfg: &mut ServiceConfig) { 135 | cfg.route("/", web::get().to(|| async { "content" })).route( 136 | "/disco", 137 | #[allow(unreachable_code)] 138 | web::get().to(|| async { 139 | panic!("the disco"); 140 | "" 141 | }), 142 | ); 143 | } 144 | 145 | #[actix_web::test] 146 | async fn report_when_panics_occur() { 147 | let triggered = Arc::new(AtomicBool::new(false)); 148 | 149 | let app = App::new() 150 | .wrap(PanicReporter::new({ 151 | let triggered = Arc::clone(&triggered); 152 | move |_| { 153 | triggered.store(true, Ordering::SeqCst); 154 | } 155 | })) 156 | .configure(configure_test_app); 157 | 158 | let app = test::init_service(app).await; 159 | 160 | let req = test::TestRequest::with_uri("/").to_request(); 161 | assert!(app.call(req).await.is_ok()); 162 | assert!(!triggered.load(Ordering::SeqCst)); 163 | 164 | let req = test::TestRequest::with_uri("/disco").to_request(); 165 | assert!( 166 | AssertUnwindSafe(app.call(req)) 167 | .catch_unwind() 168 | .await 169 | .is_err() 170 | ); 171 | assert!(triggered.load(Ordering::SeqCst)); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /actix-web-lab/src/catch_panic.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::{Ready, ready}, 3 | panic::AssertUnwindSafe, 4 | rc::Rc, 5 | }; 6 | 7 | use actix_web::{ 8 | dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready}, 9 | error, 10 | }; 11 | use futures_core::future::LocalBoxFuture; 12 | use futures_util::FutureExt as _; 13 | 14 | /// A middleware to catch panics in wrapped handlers and middleware, returning empty 500 responses. 15 | /// 16 | /// **This middleware should never be used as replacement for proper error handling.** See [this 17 | /// thread](https://github.com/actix/actix-web/issues/1501#issuecomment-627517783) for historical 18 | /// discussion on why Actix Web does not do this by default. 19 | /// 20 | /// It is recommended that this middleware be registered last. That is, `wrap`ed after everything 21 | /// else except `Logger`. 22 | /// 23 | /// # Examples 24 | /// 25 | /// ``` 26 | /// # use actix_web::App; 27 | /// use actix_web_lab::middleware::CatchPanic; 28 | /// 29 | /// App::new().wrap(CatchPanic::default()) 30 | /// # ; 31 | /// ``` 32 | /// 33 | /// ```no_run 34 | /// # use actix_web::App; 35 | /// use actix_web::middleware::{Logger, NormalizePath}; 36 | /// use actix_web_lab::middleware::CatchPanic; 37 | /// 38 | /// // recommended wrap order 39 | /// App::new() 40 | /// .wrap(NormalizePath::default()) 41 | /// .wrap(CatchPanic::default()) // <- after everything except logger 42 | /// .wrap(Logger::default()) 43 | /// # ; 44 | /// ``` 45 | #[derive(Debug, Clone, Default)] 46 | #[non_exhaustive] 47 | pub struct CatchPanic; 48 | 49 | impl Transform for CatchPanic 50 | where 51 | S: Service, Error = actix_web::Error> + 'static, 52 | { 53 | type Response = ServiceResponse; 54 | type Error = actix_web::Error; 55 | type Transform = CatchPanicMiddleware; 56 | type InitError = (); 57 | type Future = Ready>; 58 | 59 | fn new_transform(&self, service: S) -> Self::Future { 60 | ready(Ok(CatchPanicMiddleware { 61 | service: Rc::new(service), 62 | })) 63 | } 64 | } 65 | 66 | /// A middleware to catch panics in wrapped handlers and middleware, returning empty 500 responses. 67 | /// 68 | /// See [`CatchPanic`]. 69 | #[doc(hidden)] 70 | #[allow(missing_debug_implementations)] 71 | pub struct CatchPanicMiddleware { 72 | service: Rc, 73 | } 74 | 75 | impl Service for CatchPanicMiddleware 76 | where 77 | S: Service, Error = actix_web::Error> + 'static, 78 | { 79 | type Response = ServiceResponse; 80 | type Error = actix_web::Error; 81 | type Future = LocalBoxFuture<'static, Result>; 82 | 83 | forward_ready!(service); 84 | 85 | fn call(&self, req: ServiceRequest) -> Self::Future { 86 | AssertUnwindSafe(self.service.call(req)) 87 | .catch_unwind() 88 | .map(move |res| match res { 89 | Ok(Ok(res)) => Ok(res), 90 | Ok(Err(svc_err)) => Err(svc_err), 91 | Err(_panic_err) => Err(error::ErrorInternalServerError("")), 92 | }) 93 | .boxed_local() 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use actix_web::{ 100 | App, Error, 101 | body::{MessageBody, to_bytes}, 102 | dev::{Service as _, ServiceFactory}, 103 | http::StatusCode, 104 | test, web, 105 | }; 106 | 107 | use super::*; 108 | 109 | fn test_app() -> App< 110 | impl ServiceFactory< 111 | ServiceRequest, 112 | Response = ServiceResponse, 113 | Config = (), 114 | InitError = (), 115 | Error = Error, 116 | >, 117 | > { 118 | App::new() 119 | .wrap(CatchPanic::default()) 120 | .route("/", web::get().to(|| async { "content" })) 121 | .route( 122 | "/disco", 123 | #[allow(unreachable_code)] 124 | web::get().to(|| async { 125 | panic!("the disco"); 126 | "" 127 | }), 128 | ) 129 | } 130 | 131 | #[actix_web::test] 132 | async fn pass_through_no_panic() { 133 | let app = test::init_service(test_app()).await; 134 | 135 | let req = test::TestRequest::default().to_request(); 136 | let res = test::call_service(&app, req).await; 137 | assert_eq!(res.status(), StatusCode::OK); 138 | let body = test::read_body(res).await; 139 | assert_eq!(body, "content"); 140 | } 141 | 142 | #[actix_web::test] 143 | async fn catch_panic_return_internal_server_error_response() { 144 | let app = test::init_service(test_app()).await; 145 | 146 | let req = test::TestRequest::with_uri("/disco").to_request(); 147 | let err = match app.call(req).await { 148 | Ok(_) => panic!("unexpected Ok response"), 149 | Err(err) => err, 150 | }; 151 | let res = err.error_response(); 152 | assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); 153 | let body = to_bytes(res.into_body()).await.unwrap(); 154 | assert!(body.is_empty()); 155 | } 156 | } 157 | --------------------------------------------------------------------------------